forked from shihaam/thijooree
add support for fahipay transfer history
This commit is contained in:
@@ -84,7 +84,7 @@ class FahipayLoginFlow {
|
|||||||
"grant_type" to "auth_id",
|
"grant_type" to "auth_id",
|
||||||
"lang" to "en",
|
"lang" to "en",
|
||||||
"version" to "2.0.0",
|
"version" to "2.0.0",
|
||||||
"platform" to "app",
|
"platform" to "BasedBank",
|
||||||
*deviceParts(deviceUuid)
|
*deviceParts(deviceUuid)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class FahipayLoginFlow {
|
|||||||
"grant_type" to "auth_id",
|
"grant_type" to "auth_id",
|
||||||
"lang" to "en",
|
"lang" to "en",
|
||||||
"version" to "2.0.0",
|
"version" to "2.0.0",
|
||||||
"platform" to "app",
|
"platform" to "BasedBank",
|
||||||
*deviceParts(deviceUuid)
|
*deviceParts(deviceUuid)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
|
|
||||||
private val displayItems = mutableListOf<Item>()
|
private val displayItems = mutableListOf<Item>()
|
||||||
private val imageCache = mutableMapOf<String, Bitmap>()
|
private val imageCache = mutableMapOf<String, Bitmap>()
|
||||||
|
private val iconUrlCache = mutableMapOf<String, Bitmap>()
|
||||||
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
||||||
|
var onIconUrlNeeded: ((url: String) -> Unit)? = null
|
||||||
|
|
||||||
fun updateImage(counterpartyName: String, bitmap: Bitmap) {
|
fun updateImage(counterpartyName: String, bitmap: Bitmap) {
|
||||||
imageCache[counterpartyName] = bitmap
|
imageCache[counterpartyName] = bitmap
|
||||||
@@ -34,6 +36,14 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateIconUrl(url: String, bitmap: Bitmap) {
|
||||||
|
iconUrlCache[url] = bitmap
|
||||||
|
displayItems.forEachIndexed { i, item ->
|
||||||
|
if (item is Item.Trx && item.transaction.iconUrl == url)
|
||||||
|
notifyItemChanged(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var _showLoadingFooter = false
|
private var _showLoadingFooter = false
|
||||||
var showLoadingFooter: Boolean
|
var showLoadingFooter: Boolean
|
||||||
get() = _showLoadingFooter
|
get() = _showLoadingFooter
|
||||||
@@ -103,7 +113,9 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
val name = trx.counterpartyName ?: trx.description
|
val name = trx.counterpartyName ?: trx.description
|
||||||
val initial = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
val initial = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
||||||
|
|
||||||
val bitmap = trx.counterpartyName?.let { imageCache[it] }
|
val iconBitmap = trx.iconUrl?.let { iconUrlCache[it] }
|
||||||
|
val contactBitmap = if (iconBitmap == null) trx.counterpartyName?.let { imageCache[it] } else null
|
||||||
|
val bitmap = iconBitmap ?: contactBitmap
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val rd = RoundedBitmapDrawableFactory.create(b.root.resources, bitmap)
|
val rd = RoundedBitmapDrawableFactory.create(b.root.resources, bitmap)
|
||||||
rd.isCircular = true
|
rd.isCircular = true
|
||||||
@@ -117,7 +129,8 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
b.fvAvatar.background = circle
|
b.fvAvatar.background = circle
|
||||||
b.tvInitial.visibility = View.VISIBLE
|
b.tvInitial.visibility = View.VISIBLE
|
||||||
b.tvInitial.text = initial
|
b.tvInitial.text = initial
|
||||||
if (trx.counterpartyName != null) onImageNeeded?.invoke(trx.counterpartyName)
|
if (trx.iconUrl != null) onIconUrlNeeded?.invoke(trx.iconUrl)
|
||||||
|
else if (trx.counterpartyName != null) onImageNeeded?.invoke(trx.counterpartyName)
|
||||||
}
|
}
|
||||||
b.tvDescription.text = trx.description
|
b.tvDescription.text = trx.description
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
@@ -22,6 +24,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
|
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibHistoryClient
|
import sh.sar.basedbank.api.mib.MibHistoryClient
|
||||||
@@ -29,6 +32,7 @@ import sh.sar.basedbank.api.mib.Transaction
|
|||||||
import sh.sar.basedbank.api.mib.TransactionCache
|
import sh.sar.basedbank.api.mib.TransactionCache
|
||||||
import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding
|
import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding
|
||||||
import sh.sar.basedbank.util.ContactImageCache
|
import sh.sar.basedbank.util.ContactImageCache
|
||||||
|
import sh.sar.basedbank.util.MerchantIconCache
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -52,9 +56,12 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
var mibTotalCount: Int = -1,
|
var mibTotalCount: Int = -1,
|
||||||
var bmlNextPage: Int = 1,
|
var bmlNextPage: Int = 1,
|
||||||
var bmlTotalPages: Int = -1,
|
var bmlTotalPages: Int = -1,
|
||||||
var cardMonthOffset: Int = 0
|
var cardMonthOffset: Int = 0,
|
||||||
|
var fahipayNextStart: Int = 0,
|
||||||
|
var fahipayTotal: Int = -1
|
||||||
) {
|
) {
|
||||||
fun hasMore(): Boolean = when {
|
fun hasMore(): Boolean = when {
|
||||||
|
account.profileType == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||||
account.profileType == "BML_PREPAID" -> cardMonthOffset < 2
|
account.profileType == "BML_PREPAID" -> cardMonthOffset < 2
|
||||||
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||||
@@ -66,6 +73,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
private var firstBatchDone = false
|
private var firstBatchDone = false
|
||||||
private val pageSize = 20
|
private val pageSize = 20
|
||||||
private val pendingImageNames = mutableSetOf<String>()
|
private val pendingImageNames = mutableSetOf<String>()
|
||||||
|
private val pendingIconUrls = mutableSetOf<String>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentTransferHistoryBinding.inflate(inflater, container, false)
|
_binding = FragmentTransferHistoryBinding.inflate(inflater, container, false)
|
||||||
@@ -75,6 +83,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
adapter = TransactionAdapter()
|
adapter = TransactionAdapter()
|
||||||
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
||||||
|
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
@@ -178,8 +187,29 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}.awaitAll().flatten())
|
}.awaitAll().flatten())
|
||||||
|
|
||||||
|
// Fahipay accounts
|
||||||
|
val fahipayStates = activeStates.filter { it.account.profileType == "FAHIPAY" }
|
||||||
|
for (state in fahipayStates) {
|
||||||
|
val session = app.fahipaySession ?: continue
|
||||||
|
try {
|
||||||
|
val flow = FahipayLoginFlow()
|
||||||
|
flow.setSessionCookie(session.sessionCookie)
|
||||||
|
val (list, total) = flow.fetchHistory(
|
||||||
|
session = session,
|
||||||
|
accountDisplayName = state.account.accountBriefName,
|
||||||
|
accountNumber = state.account.accountNumber,
|
||||||
|
start = state.fahipayNextStart
|
||||||
|
)
|
||||||
|
if (total > 0) state.fahipayTotal = total
|
||||||
|
state.fahipayNextStart += list.size
|
||||||
|
results.addAll(list)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
||||||
val mibStates = activeStates.filter { !it.account.profileType.startsWith("BML") }
|
val mibStates = activeStates.filter {
|
||||||
|
!it.account.profileType.startsWith("BML") && it.account.profileType != "FAHIPAY"
|
||||||
|
}
|
||||||
for ((profileId, states) in mibStates.groupBy { it.account.profileId }) {
|
for ((profileId, states) in mibStates.groupBy { it.account.profileId }) {
|
||||||
val session = mibSession ?: break
|
val session = mibSession ?: break
|
||||||
app.mibMutex.withLock {
|
app.mibMutex.withLock {
|
||||||
@@ -265,6 +295,28 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadMerchantIcon(url: String) {
|
||||||
|
if (!pendingIconUrls.add(url)) return
|
||||||
|
val cached = MerchantIconCache.load(requireContext(), url)
|
||||||
|
if (cached != null) {
|
||||||
|
binding.recyclerView.post { adapter.updateIconUrl(url, cached) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||||
|
val bytes = response.body?.bytes() ?: return@launch
|
||||||
|
response.close()
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||||
|
MerchantIconCache.save(requireContext(), url, bitmap)
|
||||||
|
withContext(Dispatchers.Main) { adapter.updateIconUrl(url, bitmap) }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
pendingIconUrls.remove(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
(activity as? HomeActivity)?.setRefreshing(false)
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ POST https://fahipay.mv/api/app/login/
|
|||||||
| `grant_type` | `auth_id` | Always `auth_id` |
|
| `grant_type` | `auth_id` | Always `auth_id` |
|
||||||
| `lang` | `en` | Always `en` |
|
| `lang` | `en` | Always `en` |
|
||||||
| `version` | `2.0.0` | App version string |
|
| `version` | `2.0.0` | App version string |
|
||||||
| `platform` | `BasedBank` | Client identifier (original app sends `app`) |
|
| `platform` | `BasedBank` | Client identifier (`app` in the original Fahipay app) |
|
||||||
| `device[available]` | `true` | See [common device fields](README.md#common-form-fields-device-info) |
|
| `device[available]` | `true` | See [common device fields](README.md#common-form-fields-device-info) |
|
||||||
| `device[platform]` | `Android` | |
|
| `device[platform]` | `Android` | |
|
||||||
| `device[uuid]` | `a1b2c3d4e5f60718` | Persistent 16-char hex UUID, generated once per install |
|
| `device[uuid]` | `a1b2c3d4e5f60718` | Persistent 16-char hex UUID, generated once per install |
|
||||||
@@ -87,7 +87,7 @@ The user has TOTP two-factor authentication enabled. Proceed to the [OTP step](0
|
|||||||
| `two_factor_method` | `string` | `"totp"` — standard TOTP (RFC 6238) |
|
| `two_factor_method` | `string` | `"totp"` — standard TOTP (RFC 6238) |
|
||||||
| `type` | `string` | `"success"` on success, `"error"` on failure |
|
| `type` | `string` | `"success"` on success, `"error"` on failure |
|
||||||
|
|
||||||
The server sets the `__Secure-sess` session cookie on this response. It must be included in all subsequent requests.
|
The `__Secure-sess` session cookie is obtained from the session initialisation step (see [Session Cookie](#session-cookie) below), not from this response.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,7 +128,14 @@ The user does not have 2FA enabled. The `authID` is returned directly — no OTP
|
|||||||
|
|
||||||
## Session Cookie
|
## Session Cookie
|
||||||
|
|
||||||
The `__Secure-sess` cookie is set by the server on the first response and must be sent on every subsequent request. It is a standard HTTP cookie with the `Secure` flag.
|
Before calling `/api/app/login/`, the client must make an initialisation request to obtain the `__Secure-sess` cookie:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://fahipay.mv/api/app/lang/data/
|
||||||
|
User-Agent: <webview UA>
|
||||||
|
```
|
||||||
|
|
||||||
|
The server sets the `__Secure-sess` cookie on this response. It must be sent with every subsequent request (login, OTP, and all authenticated calls). It is a standard HTTP cookie with the `Secure` flag:
|
||||||
|
|
||||||
```
|
```
|
||||||
Set-Cookie: __Secure-sess=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; Path=/; Secure; HttpOnly; SameSite=Strict
|
Set-Cookie: __Secure-sess=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; Path=/; Secure; HttpOnly; SameSite=Strict
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ POST https://fahipay.mv/api/app/otp/
|
|||||||
| `grant_type` | `auth_id` | Always `auth_id` |
|
| `grant_type` | `auth_id` | Always `auth_id` |
|
||||||
| `lang` | `en` | Always `en` |
|
| `lang` | `en` | Always `en` |
|
||||||
| `version` | `2.0.0` | App version string |
|
| `version` | `2.0.0` | App version string |
|
||||||
| `platform` | `BasedBank` | Client identifier |
|
| `platform` | `BasedBank` | Client identifier (`app` in the original Fahipay app) |
|
||||||
| `device[available]` | `true` | Same device fields as login — must match |
|
| `device[available]` | `true` | Same device fields as login — must match |
|
||||||
| `device[platform]` | `Android` | |
|
| `device[platform]` | `Android` | |
|
||||||
| `device[uuid]` | `a1b2c3d4e5f60718` | Must be the **same UUID** used in the login request |
|
| `device[uuid]` | `a1b2c3d4e5f60718` | Must be the **same UUID** used in the login request |
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ The `device[uuid]` must be consistent across all requests from the same install.
|
|||||||
|
|
||||||
```
|
```
|
||||||
Client Server
|
Client Server
|
||||||
|
| |
|
||||||
|
| GET /api/app/lang/data/ | ← session init (obtains __Secure-sess cookie)
|
||||||
|
|---------------------------------->|
|
||||||
|
| Set-Cookie: __Secure-sess=... |
|
||||||
|
|<----------------------------------|
|
||||||
| |
|
| |
|
||||||
| POST /api/app/login/ |
|
| POST /api/app/login/ |
|
||||||
| { email=IDCARD, password, ... } |
|
| { email=IDCARD, password, ... } |
|
||||||
|
|||||||
Reference in New Issue
Block a user