Always-visible burn-rate banner that follows the user's edits - #108
Merged
Conversation
The spend figures existed but only on the Dashboard tab, so the number was out of sight exactly when a user was adding the subscription that changes it. This puts a one-line summary in Shell.TitleView - "USD 12.50/wk | 54.17/mo | 650.00/yr" - visible on every screen, and refreshes it whenever something that feeds it changes. Refresh-on-mutation, not polling and not a push channel. This account's spend only changes when this user acts, so there is no second writer to hear about. SubscriptionsChangedMessage is published after a successful create, update or delete, and after a home-currency change in Settings, which moves every figure without a subscription being touched. A failed save publishes nothing - the headline must not move for a write the server rejected. Uses IMessenger from CommunityToolkit.Mvvm, already a dependency, rather than a hand-rolled event bus. Injected rather than the static Default so each test gets an isolated instance. DashboardViewModel becomes the one singleton view model: the banner and the dashboard page bind to the same instance, so one fetch feeds both and they cannot drift apart. Summary is derived from the existing figures with NotifyPropertyChangedFor, so there is no second copy of the numbers to keep in sync, and its emptiness doubles as the banner's visibility - no separate flag. SessionEndedMessage clears the figures on sign-out and on an unrecoverable 401. The login page lives inside the same Shell, so without this one user's spend would still be on screen behind the next person's sign-in form. The offline notice keeps its place in the same strip, layered over the summary: the title bar has room for one line, and "these numbers may be stale" is the more urgent thing to say when both are true. The existing offline burn-rate cache means the banner keeps showing the last known figures rather than going blank. No AI anywhere in this: the arithmetic is cost / cycle_days summed and projected, and it already lives server-side in BurnRateCalculator per the architectural rule that burn-rate math is not duplicated in the client. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Puts the spend figures in
Shell.TitleViewso they're on screen everywhere, and refreshes them whenever the user does something that changes them.No AI involved. The arithmetic is
cost / cycle_dayssummed and projected, and it already lives server-side inBurnRateCalculator— per the architectural rule that burn-rate math is never duplicated in the client. This PR is presentation and refresh plumbing only; not one line of the calculation changed.The problem
The figures existed, but only on the Dashboard tab — out of sight at exactly the moment a user is on the Subscriptions tab adding the thing that changes them. Each page reloads in
OnAppearing, so the number was correct but only once you navigated back to it.How "dynamic" works
Refresh-on-mutation. Not polling, not SignalR: this account's spend only changes when this user acts, so there's no second writer to hear about.
SubscriptionsChangedMessageis published after:SubscriptionDetailViewModel.SaveAsyncSubscriptionListViewModel.DeleteSubscriptionAsyncSettingsViewModel.SaveAsyncThat last one matters and is easy to miss — every figure is converted into the home currency, so switching it moves all three numbers without a single subscription being touched.
A failed save publishes nothing. The headline must not move for a write the server rejected; there's a test for it.
Design notes
IMessengerfromCommunityToolkit.Mvvm, already a dependency — no hand-rolled event bus. Injected rather than using the staticWeakReferenceMessenger.Default, so each test gets an isolated instance and nothing leaks between them.DashboardViewModelbecomes the one singleton view model. The banner andDashboardPagebind to the same instance, so one fetch feeds both and they can't drift apart. Everything else stays transient.Summaryis derived, not stored.[NotifyPropertyChangedFor(nameof(Summary))]on the four properties that feed it means there's no second copy of the numbers to keep in sync. Its emptiness doubles as the banner's visibility via the toolkit'sIsStringNotNullOrEmptyConverter— no separateIsVisibleflag to forget.SessionEndedMessageclears the figures on sign-out and on an unrecoverable 401. The login page lives inside the same Shell, so without this one user's spend would still be sitting above the next person's sign-in form.The offline notice keeps its place, layered over the summary in the same
Grid. The title bar has room for one line, and "these numbers may be stale" is the more urgent thing to say when both are true. The existing offline burn-rate cache means the banner shows last-known figures rather than going blank.Tests
tests/SubVora.Mobile.Tests/BurnRateBannerTests.cs, 10 cases:PropertyChangedfires forSummarywhen the figures moveSubscriptionsChangedMessagetriggers a refetch and the string updatesSessionEndedMessageclears figures, currency, and the category breakdownSessionEndedMessageThe currency-change test earned its place immediately — it caught the publish landing in
LoadAsyncinstead ofSaveAsync(both methods contain the same two assignment lines).What I could not verify
The XAML binding is compile-checked only —
x:DataTypeon the Shell with compiled bindings means a typo inSummarywould fail the build, and it doesn't. But I did not run the GUI, so the strip's appearance on a real device is unverified:Shell.TitleViewsizing differs between Android and iOS, and the summary may want a smaller font or a shorter format on a narrow screen. Worth a look on a device before shipping.Follow-on
TitleViewfits one short line, so the full "You are spending $X per week" sentence stays on the Dashboard page — unchanged by this PR. If you'd rather have the long form always visible, that needs a different container than the title bar.