Skip to content

fix(savedcards): refresh card list after delete and wire pull-to-refresh#2037

Open
bhuvan-somisetty wants to merge 2 commits into
openMF:devfrom
bhuvan-somisetty:fix/savedcards-delete-stale-state
Open

fix(savedcards): refresh card list after delete and wire pull-to-refresh#2037
bhuvan-somisetty wants to merge 2 commits into
openMF:devfrom
bhuvan-somisetty:fix/savedcards-delete-stale-state

Conversation

@bhuvan-somisetty

@bhuvan-somisetty bhuvan-somisetty commented May 15, 2026

Copy link
Copy Markdown

Deleting a saved card left the list showing the deleted entry until you navigated away and back. The root cause was that cardState was built directly from a one-shot API flow: once it emitted and completed, there was nothing to trigger a re-fetch after a delete.

The fix introduces a MutableSharedFlow<Unit> as a refresh trigger (seeded with an initial emission so the first load still happens automatically). The cardState flow now uses flatMapLatest on that trigger, so every tryEmit(Unit) restarts the underlying getSavedCards call and the list updates immediately.

Two places now emit to the trigger:

  • After a successful delete, so the removed card disappears right away
  • When CardAction.RefreshCards is dispatched, which the screen sends on pull-to-refresh

The isPullRefreshEnabled property was already defined on ViewState but the scaffold was never wired up to it. This PR connects it via rememberMifosPullToRefreshState, so users can also manually refresh the list by pulling down.

The known-issue comment on CardsScreen is removed since the underlying problem is resolved.

Tested manually: deleting a card now removes it from the list without any navigation, and pull-to-refresh re-fetches correctly.

Summary by CodeRabbit

  • New Features
    • Pull-to-refresh support added to saved cards screen—swipe down to manually refresh your card list.
    • Saved cards automatically refresh after successful deletion.

Review Change Stack

@bhuvan-somisetty bhuvan-somisetty requested a review from a team May 15, 2026 18:12
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@bhuvan-somisetty, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1997b28d-ff63-425a-bf47-cf9a560f6d71

📥 Commits

Reviewing files that changed from the base of the PR and between 7e69208 and f29ff45.

📒 Files selected for processing (2)
  • feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreen.kt
  • feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt
📝 Walkthrough

Walkthrough

Pull-to-refresh functionality is added to the saved cards feature. The ViewModel introduces a refresh trigger that re-fetches cards on explicit refresh or after deletion. The UI wires the scaffold to enable pull-to-refresh and dispatch the refresh action.

Changes

Pull-to-Refresh for Saved Cards

Layer / File(s) Summary
Refresh trigger mechanism and action contract
feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt
CardAction.RefreshCards is added as a new action. A private refreshTrigger MutableSharedFlow<Unit> is introduced, and cardState is refactored to use flatMapLatest to re-subscribe to the repository on each trigger emission. The refresh action is handled by emitting to the trigger.
Refresh on card deletion
feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt
After successful card deletion, the ViewModel emits to refreshTrigger to reload the saved cards.
UI scaffold pull-to-refresh wiring
feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreen.kt
rememberMifosPullToRefreshState is imported and wired into MifosScaffold with refresh enabled based on state.isPullRefreshEnabled and callback routed to CardAction.RefreshCards. A stale "Known Issue" comment is removed.

Sequence Diagram

sequenceDiagram
  participant User
  participant CardsScreen
  participant CardsScreenViewModel
  participant Repository
  User->>CardsScreen: Pull to refresh
  CardsScreen->>CardsScreenViewModel: CardAction.RefreshCards
  CardsScreenViewModel->>CardsScreenViewModel: emit to refreshTrigger
  CardsScreenViewModel->>Repository: getSavedCards()
  Repository-->>CardsScreenViewModel: cards
  CardsScreenViewModel-->>CardsScreen: cardState updated
  CardsScreen-->>User: Display refreshed cards
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A flutter of cards in the breeze,
Pull down to refresh with ease,
The trigger now flows through the state,
No stale data, we refresh first-rate! 🐰
Deletion too gets the refresh call,
Pull-to-refresh handles it all!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing stale card list state after deletion and implementing pull-to-refresh functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt`:
- Line 106: CardAction.RefreshCards handling uses refreshTrigger.tryEmit(Unit)
which can silently drop events; replace both unchecked tryEmit calls (the one
handling CardAction.RefreshCards and the other at the later occurrence) with a
suspending emit invoked from the ViewModel's coroutine scope (e.g., launch {
refreshTrigger.emit(Unit) }) so the emission will suspend until buffer space is
available; update the CardsScreenViewModel to launch a coroutine on its existing
scope (e.g., viewModelScope / viewModelCoroutineScope / the class's
CoroutineScope) and call refreshTrigger.emit(Unit) for both refresh locations to
ensure refreshes are not lost and cardState stays consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a73804af-18e6-4a80-a32a-ec286eff2788

📥 Commits

Reviewing files that changed from the base of the PR and between e8303e2 and 7e69208.

📒 Files selected for processing (2)
  • feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreen.kt
  • feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt

}
}

is CardAction.RefreshCards -> refreshTrigger.tryEmit(Unit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In kotlinx.coroutines MutableSharedFlow, under what conditions can tryEmit return false (especially with replay = 1 and default onBufferOverflow), and is emit recommended when refresh events must not be dropped?

💡 Result:

In Kotlin's MutableSharedFlow, the tryEmit function returns false only when the BufferOverflow strategy is set to SUSPEND (the default) and there are active subscribers that are not ready to accept the new value [1][2][3]. When you configure a MutableSharedFlow with replay = 1 and the default onBufferOverflow = BufferOverflow.SUSPEND, tryEmit will return false if the internal buffer is full and at least one subscriber is currently unable to process the emission [1][4][5]. Because the default extraBufferCapacity is 0, the flow has no additional room to buffer values beyond the replay cache [4][6]. If a subscriber is slow or busy, the flow cannot immediately place the new value, causing tryEmit to fail [6][5]. If your application requires that refresh events must not be dropped, using tryEmit is not recommended [6][5]. Instead, you should use the suspending emit function [5][7]. The emit function will suspend the producer coroutine until there is sufficient buffer space available to deliver the value to all subscribers, ensuring that no events are lost due to backpressure [8][2][5]. To safely handle events that must not be dropped: 1. Use emit to ensure the producer waits for subscribers [5][7]. 2. If you must use tryEmit (e.g., in a non-suspending context), ensure you have configured sufficient extraBufferCapacity to handle expected bursts of events, though this only delays the point at which backpressure occurs rather than eliminating it [6][5]. 3. If the producer cannot suspend, consider using a different architecture, such as a Channel or a dedicated event bus, that better aligns with your specific requirements for event delivery and backpressure [5].

Citations:


🏁 Script executed:

# First, find the exact file location
find . -name "CardsScreenViewModel.kt" -type f 2>/dev/null

Repository: openMF/mifos-pay

Length of output: 159


🏁 Script executed:

cat -n "./feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt"

Repository: openMF/mifos-pay

Length of output: 10058


Use non-lossy refresh emission instead of unchecked tryEmit.

Both refresh paths at lines 106 and 159 ignore tryEmit's boolean result. With MutableSharedFlow<Unit>(replay = 1) and default BufferOverflow.SUSPEND, if a subscriber is slow or the internal buffer fills, tryEmit silently fails and drops the refresh event. Since cardState depends on refreshTrigger to refresh the card list, dropped emissions will leave stale data on screen—undoing the fix this PR provides.

Replace with the non-blocking suspending emit pattern below, which waits for buffer space instead of silently dropping:

🔧 Proposed fix
+    private fun requestRefresh() {
+        viewModelScope.launch {
+            refreshTrigger.emit(Unit)
+        }
+    }

     override fun handleAction(action: CardAction) {
         when (action) {
             // ...
-            is CardAction.RefreshCards -> refreshTrigger.tryEmit(Unit)
+            is CardAction.RefreshCards -> requestRefresh()
             // ...
         }
     }

     private fun handleCardDeleteResult(action: HandleCardDeleteResult) {
         when (action.result) {
             is DataState.Success -> {
                 mutableStateFlow.update {
                     it.copy(dialogState = null)
                 }
-                refreshTrigger.tryEmit(Unit)
+                requestRefresh()
                 sendEvent(CardEvent.ShowToast(action.result.data))
             }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
is CardAction.RefreshCards -> refreshTrigger.tryEmit(Unit)
private fun requestRefresh() {
viewModelScope.launch {
refreshTrigger.emit(Unit)
}
}
override fun handleAction(action: CardAction) {
when (action) {
// ...
is CardAction.RefreshCards -> requestRefresh()
// ...
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/savedcards/src/commonMain/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt`
at line 106, CardAction.RefreshCards handling uses refreshTrigger.tryEmit(Unit)
which can silently drop events; replace both unchecked tryEmit calls (the one
handling CardAction.RefreshCards and the other at the later occurrence) with a
suspending emit invoked from the ViewModel's coroutine scope (e.g., launch {
refreshTrigger.emit(Unit) }) so the emission will suspend until buffer space is
available; update the CardsScreenViewModel to launch a coroutine on its existing
scope (e.g., viewModelScope / viewModelCoroutineScope / the class's
CoroutineScope) and call refreshTrigger.emit(Unit) for both refresh locations to
ensure refreshes are not lost and cardState stays consistent.

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
tryEmit on a replay=1 SharedFlow with default BufferOverflow.SUSPEND can
silently drop the refresh event if a subscriber isn't ready, leaving
stale data on screen. Switch both refresh call sites to a suspending
emit via a shared helper so the event is never dropped.
@bhuvan-somisetty bhuvan-somisetty force-pushed the fix/savedcards-delete-stale-state branch from def4cf3 to f29ff45 Compare July 5, 2026 05:58
@bhuvan-somisetty

Copy link
Copy Markdown
Author

Rebased on latest dev and pushed a fix for the CodeRabbit finding: both refresh call sites now go through a requestRefresh() helper that uses the suspending emit instead of tryEmit, so a refresh event can no longer be silently dropped under backpressure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant