better network error handling, fix crash when no network in transaction history page

This commit is contained in:
2026-05-28 00:14:11 +05:00
parent 0560c53ae3
commit c4d3c1efd4
7 changed files with 78 additions and 33 deletions
@@ -150,8 +150,15 @@ class AccountHistoryFragment : Fragment() {
pendingIconUrls.clear() pendingIconUrls.clear()
firstPageDone = false firstPageDone = false
fetcher = HistoryFetcher(account) fetcher = HistoryFetcher(account)
adapter.setTransactions(emptyList()) // Restore cache immediately so data stays visible while refreshing
binding.emptyView.visibility = View.GONE val cached = TransactionCache.load(requireContext(), account.accountNumber)
if (cached.isNotEmpty()) {
allTransactions.addAll(cached)
filterAndDisplay()
} else {
adapter.setTransactions(emptyList())
binding.emptyView.visibility = View.GONE
}
loadNextPage() loadNextPage()
} }
@@ -179,6 +186,7 @@ class AccountHistoryFragment : Fragment() {
} }
isLoading = false isLoading = false
if (_binding == null) return@launch
if (!firstPageDone) { if (!firstPageDone) {
firstPageDone = true firstPageDone = true
@@ -191,6 +199,7 @@ class AccountHistoryFragment : Fragment() {
if (allTransactions.isEmpty()) binding.emptyView.visibility = View.VISIBLE if (allTransactions.isEmpty()) binding.emptyView.visibility = View.VISIBLE
return@launch return@launch
} }
(activity as? HomeActivity)?.hideConnectivityBanner()
if (transactions.isNotEmpty()) { if (transactions.isNotEmpty()) {
val existingIds = allTransactions.map { it.id }.toHashSet() val existingIds = allTransactions.map { it.id }.toHashSet()
@@ -147,7 +147,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() } viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() } viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
(activity as? HomeActivity)?.loadAllContacts() (activity as? HomeActivity)?.triggerRefresh()
} }
private fun attachMediator(pages: List<TabDef>) { private fun attachMediator(pages: List<TabDef>) {
@@ -137,7 +137,7 @@ class ContactsFragment : Fragment() {
binding.swipeRefresh.setOnRefreshListener { binding.swipeRefresh.setOnRefreshListener {
contactsRefreshing = true contactsRefreshing = true
(activity as? HomeActivity)?.loadAllContacts() (activity as? HomeActivity)?.triggerRefresh()
} }
viewModel.contactCategories.observe(viewLifecycleOwner) { cats -> viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
@@ -47,7 +47,7 @@ class FinancingFragment : Fragment() {
binding.swipeRefresh.setOnRefreshListener { binding.swipeRefresh.setOnRefreshListener {
financingRefreshing = true financingRefreshing = true
(activity as? HomeActivity)?.triggerRefreshFinancing() (activity as? HomeActivity)?.triggerRefresh()
} }
viewModel.accounts.observe(viewLifecycleOwner) { rebuildAdapter() } viewModel.accounts.observe(viewLifecycleOwner) { rebuildAdapter() }
@@ -554,7 +554,7 @@ fun applyNavLabelVisibility() {
binding.connectivityBanner.visibility = View.VISIBLE binding.connectivityBanner.visibility = View.VISIBLE
} }
private fun hideConnectivityBanner() { fun hideConnectivityBanner() {
binding.connectivityBanner.visibility = View.GONE binding.connectivityBanner.visibility = View.GONE
} }
@@ -859,7 +859,11 @@ class TransferFragment : Fragment() {
activity.triggerRefresh() activity.triggerRefresh()
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar)) activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
} else if (!ok) { } else if (!ok) {
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show() if (msg == "CONNECTIVITY") {
(activity as? HomeActivity)?.showConnectivityBanner(getString(R.string.connectivity_no_internet))
} else {
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
}
} }
} }
} }
@@ -1069,7 +1073,7 @@ class TransferFragment : Fragment() {
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null) Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
} }
} catch (e: Exception) { } catch (e: Exception) {
Triple(false, e.message ?: "Transfer failed", null) Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null)
} }
} }
@@ -1122,7 +1126,7 @@ class TransferFragment : Fragment() {
// Step 1: initiate // Step 1: initiate
val initiated = try { val initiated = try {
BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank) BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
} catch (e: Exception) { return Triple(false, e.message ?: "Initiation failed", null) } } catch (e: Exception) { return Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Initiation failed"), null) }
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null) if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
@@ -1154,7 +1158,7 @@ class TransferFragment : Fragment() {
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null) Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
} }
} catch (e: Exception) { } catch (e: Exception) {
Triple(false, e.message ?: "Transfer failed", null) Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null)
} }
} }
@@ -1396,7 +1400,7 @@ class TransferFragment : Fragment() {
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null as TransferReceiptData?) Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null as TransferReceiptData?)
} }
} catch (e: Exception) { } catch (e: Exception) {
Triple(false, e.message ?: "Transfer failed", null as TransferReceiptData?) Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null as TransferReceiptData?)
} }
} }
(activity as? HomeActivity)?.setRefreshing(false) (activity as? HomeActivity)?.setRefreshing(false)
@@ -1410,7 +1414,11 @@ class TransferFragment : Fragment() {
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar)) activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
} else { } else {
binding.btnTransfer.isEnabled = true binding.btnTransfer.isEnabled = true
binding.tilBmlOtp.error = msg if (msg == "CONNECTIVITY") {
(activity as? HomeActivity)?.showConnectivityBanner(getString(R.string.connectivity_no_internet))
} else {
binding.tilBmlOtp.error = msg
}
} }
} }
} }
@@ -30,6 +30,7 @@ import sh.sar.basedbank.api.fahipay.FahipayHistoryClient
import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.models.BankAccount
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
import sh.sar.basedbank.api.models.BankServerException
import sh.sar.basedbank.api.models.BankTransaction import sh.sar.basedbank.api.models.BankTransaction
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
@@ -161,8 +162,15 @@ class TransferHistoryFragment : Fragment() {
val accounts = accountStates.map { it.account } val accounts = accountStates.map { it.account }
accountStates.clear() accountStates.clear()
accounts.forEach { accountStates.add(AccountState(it)) } accounts.forEach { accountStates.add(AccountState(it)) }
adapter.setTransactions(emptyList()) // Restore cache immediately so data stays visible while refreshing
binding.emptyView.visibility = View.GONE val cached = TransactionCache.load(requireContext(), "transfer")
if (cached.isNotEmpty()) {
allTransactions.addAll(cached)
filterAndDisplay()
} else {
adapter.setTransactions(emptyList())
binding.emptyView.visibility = View.GONE
}
loadNextPages() loadNextPages()
} }
@@ -178,6 +186,14 @@ class TransferHistoryFragment : Fragment() {
val app = requireActivity().application as BasedBankApp val app = requireActivity().application as BasedBankApp
lifecycleScope.launch { lifecycleScope.launch {
val bannerMsg = java.util.concurrent.atomic.AtomicReference<String?>(null)
fun trackError(e: Exception) {
when {
e is java.io.IOException -> bannerMsg.compareAndSet(null, "IO")
e is BankServerException -> bannerMsg.compareAndSet(null, "SERVER:${e.bankName}")
}
}
val newTransactions = withContext(Dispatchers.IO) { val newTransactions = withContext(Dispatchers.IO) {
val results = mutableListOf<BankTransaction>() val results = mutableListOf<BankTransaction>()
@@ -215,7 +231,7 @@ class TransferHistoryFragment : Fragment() {
list list
} }
} }
} catch (_: Exception) { emptyList<BankTransaction>() } } catch (e: Exception) { trackError(e); emptyList<BankTransaction>() }
} }
}.awaitAll().flatten()) }.awaitAll().flatten())
@@ -233,7 +249,7 @@ class TransferHistoryFragment : Fragment() {
if (total > 0) state.fahipayTotal = total if (total > 0) state.fahipayTotal = total
state.fahipayNextStart += list.size state.fahipayNextStart += list.size
results.addAll(list) results.addAll(list)
} catch (_: Exception) {} } catch (e: Exception) { trackError(e) }
} }
// MIB accounts: serialized per profile, protected by mutex to prevent session race // MIB accounts: serialized per profile, protected by mutex to prevent session race
@@ -242,23 +258,25 @@ class TransferHistoryFragment : Fragment() {
val session = app.mibSessions[loginId] ?: continue val session = app.mibSessions[loginId] ?: continue
for ((profileId, states) in loginStates.groupBy { it.account.profileId }) { for ((profileId, states) in loginStates.groupBy { it.account.profileId }) {
app.mibMutex.withLock { app.mibMutex.withLock {
val profiles = app.mibProfilesMap[loginId] ?: emptyList() try {
val profile = profiles.firstOrNull { it.profileId == profileId } val profiles = app.mibProfilesMap[loginId] ?: emptyList()
if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile) val profile = profiles.firstOrNull { it.profileId == profileId }
for (state in states) { if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile)
try { for (state in states) {
val (list, total) = MibHistoryClient().fetchHistory( try {
session = session, val (list, total) = MibHistoryClient().fetchHistory(
accountNo = state.account.accountNumber, session = session,
accountDisplayName = state.account.accountBriefName, accountNo = state.account.accountNumber,
start = state.mibNextStart, accountDisplayName = state.account.accountBriefName,
pageSize = pageSize start = state.mibNextStart,
) pageSize = pageSize
if (total > 0) state.mibTotalCount = total )
state.mibNextStart += list.size.coerceAtLeast(pageSize) if (total > 0) state.mibTotalCount = total
results.addAll(list) state.mibNextStart += list.size.coerceAtLeast(pageSize)
} catch (_: Exception) {} results.addAll(list)
} } catch (e: Exception) { trackError(e) }
}
} catch (e: Exception) { trackError(e) }
} }
} }
} }
@@ -267,6 +285,16 @@ class TransferHistoryFragment : Fragment() {
} }
isLoading = false isLoading = false
if (_binding == null) return@launch
val raw = bannerMsg.get()
when {
raw == null -> (activity as? HomeActivity)?.hideConnectivityBanner()
raw == "IO" -> (activity as? HomeActivity)?.showConnectivityBanner(getString(R.string.connectivity_no_internet))
raw.startsWith("SERVER:") -> (activity as? HomeActivity)?.showConnectivityBanner(
getString(R.string.connectivity_server_error, raw.removePrefix("SERVER:"))
)
}
if (!firstBatchDone) { if (!firstBatchDone) {
firstBatchDone = true firstBatchDone = true