chore: post-refactor housekeeping + P1 fixes (#59, #71, #72)#75
chore: post-refactor housekeeping + P1 fixes (#59, #71, #72)#75variablefate merged 5 commits intomainfrom
Conversation
Updates the Quick Implementation Status table for the #65/#66/#67 arc that landed in PRs #70, #69, #68, #73, and adds two new subsections to Key Files Reference: - Coordinators (:common/coordinator/) — catalogs the six extracted coordinators, notes the SDK-grade posture, flags them as synthetic hotspots warranting extra care on first bug fixes. - Screen Components (Issue #67) — catalogs the per-module components/ packages that now own the decomposed Compose screens. Also annotates the Ride State Management entries to note that ride protocol logic has moved out of the ViewModels into the coordinators (accept* methods are now delegates). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In SATS display mode, `formatFareAmount` (RideTabComponents) and `formatFareUsd` (DriverSelectionScreen) silently fell back to a `$X.XX` USD string when `BitcoinPriceService.btcPriceUsd` was null. To the user — who had explicitly toggled SATS — the fare surface looked identical to USD mode, so tapping the currency toggle (USD → SATS → USD) produced no visible change and the click target appeared broken. Cold starts, relay outages, and network blips all hit this path. Replaces both fallbacks with `formatSatsOrPlaceholder`, a new helper in :common/fiat/FiatFareFormatting that returns `"— sats"` (em-dash) when the sats value is null. The placeholder is visually distinct from any `$X.XX` formatting — users can see their toggle was applied and understand conversion is pending instead of mistaking the output for a dollar amount. `HistoryStatsCard.totalSpentDisplay` and `RideHistoryEntry.formatFareDisplay` do not have this bug — their SATS branches read a known-present `fareSats` field directly and never cross-format into USD. Closes #72 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the five named hotspots from Issue #71's third-pass review of #68: 1. **Ticker hoisted to leaf.** `RelativeTimeText(timestampSeconds, ...)` is a new private composable that owns the 1Hz ticker's state. `RideOfferCard` and `BroadcastRideRequestCard` now render it once in their header; the ticker's per-second recomposition is scoped to the `Text` itself instead of cascading through the whole card body (route math, earnings, payment-method lookup). 2. **Derived values wrapped in `remember`.** The nullable route unboxing (`pickupDistanceKm`, `pickupDurationMin`, etc.), the authoritative-USD-fare extraction, the earnings `$/hr` + `$/mi` computations, and the RoadFlare payment-method `findBestCommonFiatMethod` scan all now run only when their real inputs change. Earnings logic factored into `computeEarningsDisplay()` + `EarningsDisplay` so both cards share one `remember` block instead of duplicating the keying. Payment-match state factored into a `PaymentMatchDisplay` sealed class so the allocation-heavy method lookup runs once per offer/methods change. 3. **`items { }` lambdas hoisted.** Each accept/decline lambda in `OfferInbox` and `RoadflareFollowerList` is `remember(offer, callback) { ... }` so child cards see the same lambda identity across recompositions. `items(..., key = { it.eventId })` added so LazyList stays aligned with underlying event IDs. The fiat-offer acceptance guard (Issue #46 interception) factored into `handleOfferAccept()` to avoid duplicating the allocation + scan logic across two call sites. 4. **Data classes annotated `@Immutable`.** `RideOfferData`, `BroadcastRideOfferData`, `FiatFare`, `RouteResult`, and `FollowedDriver` all carry `List<T>` fields or nested lists that cause the Compose compiler to infer unstable-by-default. Each is a pure value type (parsed from a Nostr event, never mutated) passed through Compose UI, so the annotation is safe and unlocks child skipping for the offer cards and follower-list rows once all other inputs stabilise. 5. **HistoryComponents and DriverCard.** Applied the same `remember(deps)` pattern to `HistoryEntryCard.fareDisplay` / `pickupDisplay` / `dropoffDisplay`, the `HistoryStatsCard.totalSpentDisplay` reducer, and `DriverCard.distanceMiles` — these shared the hotspot pattern even though they're not on the 1Hz critical path. Not fixed in this PR: Map/List parameter stability at the OfferInbox / RoadflareFollowerList boundary. kotlin.collections.Map is intrinsically unstable; fixing that requires either adding `kotlinx.collections.immutable` (a cross-cutting dependency) or defining `@Immutable` wrapper classes for every Map/List param in the UiState. Out of scope for a single-issue perf pass — filed as a follow-up observation in the PR body. Closes #71 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rider-side cancel warning dialog (shown when cancelling after PIN verification) always said "Your payment has already been authorized to the driver. If you cancel now, the driver can still claim the fare." — but that only applies to Cashu HTLC rides (SAME_MINT / CROSS_MINT payment paths). Fiat payment rides (Zelle, Venmo, Cash App, cash) and RoadFlare bitcoin/fiat rides have no escrow for the driver to claim, so the escrow-claim language was misleading. `CancelDialogStack` now takes the ride's `PaymentPath` and branches: - **Cashu HTLC (SAME_MINT / CROSS_MINT):** unchanged — "Payment Already Sent / driver can still claim the fare / cancelling does not guarantee a refund." - **Fiat / NO_PAYMENT:** "Cancel This Ride? / Your driver has been notified and may already be on the way. Cancelling now may inconvenience them." No refund/claim language since there was no payment authorization. Trigger condition (`preimageShared || pinVerified`) is unchanged — `pinVerified` still surfaces the dialog for fiat rides, just with the correct text. Closes #59 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First-pass review feedback on PR #75. Two fixes bundled: **1. Stabilize per-offer callbacks at `DriverModeScreen`.** The #71 changes added `remember(offer, onAcceptOffer) { { onAcceptOffer(offer) } }` wrappers on child-side lambdas in `OfferInbox` / `RoadflareFollowerList` to enable `items { }` child skipping. But the parent passed fresh `{ viewModel.acceptOffer(it) }` lambdas per recompose, so the child `remember` invalidated every frame — strictly worse than inline lambdas. Hoist six per-offer/per-broadcast callbacks to `remember(viewModel) { ... }` at the screen level. `viewModel` is the only key needed (ViewModel instances outlive recomposition), so these stay identity-stable across frames and the child-side `remember`s now actually hold. Unblocks the intended child-skip path in the offer inbox. **2. Replace `Triple` destructuring with direct `val` assignments in `CancelDialogStack`.** Positional destructuring (`val (title, body, footer) = ...`) communicates nothing about what each position means and allocates a `Triple` on every recomposition of the dialog. Replace with three explicit `val title: String` / `val body: String` / `val footer: String?` declarations assigned inside the if/else — clearer and no allocation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review — First passScanned the PR with five parallel agents (CLAUDE.md compliance, shallow bug scan, git-history review, prior-PR feedback, code-comment compliance). Surfaced five findings. Using the verify-and-fix filter (improve-codebase / lower-entropy / fix-bug; any yes → advance): Advanced + fixed in 5e43ae3:
Verified false-positive (no change needed):
Build green on 5e43ae3 ( 🤖 Generated with Claude Code |
Code review — Pass 2 (convergence)Pass 2 focused on the 5e43ae3 review-fix commit. No new issues:
Converged. Marking ready-for-review. 🤖 Generated with Claude Code |
Code review — Third passFresh scan against tip 5e43ae3 with five parallel agents (CLAUDE.md compliance, shallow bug scan, git-history review, prior-PR feedback, code-comment compliance). New findings: none. Specific things spot-checked this pass:
Converged. Keeping PR ready-for-review. 🤖 Generated with Claude Code |
Housekeeping + three deferred P1 fixes from the #65/#66/#67 refactor arc. Split into focused commits for easier review.
Scope
Code/docs
docs: reflect post-refactor coordinator + screen-component architecture—.claude/CLAUDE.mdupdated to reflect the four refactor PRs that landed today (feat(coordinator): extract rider protocol logic into :common coordinators #70, refactor(drivestr): extract AvailabilityCoordinator, AcceptanceCoordinator, RoadflareDriverCoordinator #69, refactor: decompose high-churn Compose screens into focused components #68, refactor(roadflare-rider): extract RideTab state-binding components #73). Adds the six new:common/coordinator/files to the Quick Implementation Status table, catalogs the per-modulecomponents/packages in Key Files Reference, and flags coordinators as synthetic hotspots requiring extra care on first bug fixes.rider-app/src/main/java/com/ridestr/rider/viewmodels/AvailabilityMonitorPolicy.ktand its test. PR feat(coordinator): extract rider protocol logic into :common coordinators #70 moved the canonical copy to:common/coordinator/; the stale copies were untracked local files flagged in the 2026-04-18 triage. Byte-diffed first — only delta is the expected API migration (RideStage→isWaitingForAcceptance: Boolean, droppedSHOW_UNAVAILABLEvariant). No commit needed since they were never tracked in git.P1 fixes
40a2e8b
fix: show placeholder in SATS mode when BTC price is unavailable (#72)— AddsformatSatsOrPlaceholderhelper in:common/fiat/FiatFareFormatting. Applied toformatFareAmount(RideTabComponents) +formatFareUsd(DriverSelectionScreen), both of which previously fell back silently to$X.XXUSD whenBitcoinPriceServicehadn't populated a price. Verified thatHistoryStatsCard.totalSpentDisplayandRideHistoryEntry.formatFareDisplaydo NOT have this bug (their SATS branch readsfareSatsdirectly). Unit test pins the contract. Closes bug: formatFareAmount SATS mode silently falls back to USD when price unavailable #72.3c0a63e
perf: reduce recomposition cost in DriverMode offer cards (#71)— Five-part fix:RelativeTimeTextleaf composable isolates the 1Hz ticker from card bodies. Route math, earnings, andfindBestCommonFiatMethodno longer re-run per second.pickupDistanceKm,pickupDurationMin, earnings strings, payment-match state) wrapped in keyedrememberblocks. Earnings logic factored intoEarningsDisplay+computeEarningsDisplay; payment-match into aPaymentMatchDisplaysealed class.items { }lambdas hoisted viaremember(offer, callback) { ... };items(key = { it.eventId })added so LazyList stays aligned.RideOfferData,BroadcastRideOfferData,FiatFare,RouteResult,FollowedDriverannotated@Immutable(pure value types whoseList<T>fields cause default-unstable inference).remember(deps)pattern applied toHistoryEntryCard.fareDisplay/pickupDisplay/dropoffDisplay,HistoryStatsCard.totalSpentDisplay, andDriverCard.distanceMiles.Not addressed in this PR:
Map<String, RouteResult>/List<T>params at theOfferInbox/RoadflareFollowerListboundary remain intrinsically unstable. Fixing that would require addingkotlinx.collections.immutable(new cross-cutting dep) or wrapper@Immutableclasses for every collection param — filed as follow-up observation below. Closes perf: Reduce recomposition cost in DriverMode offer cards #71.035c51c
fix: contextual cancel-warning text for fiat/RoadFlare rides (#59)—CancelDialogStacknow takes aPaymentPathparameter. Cashu HTLC rides (SAME_MINT / CROSS_MINT) keep the existing "driver can still claim the fare / no refund guaranteed" text. Fiat / NO_PAYMENT rides get new text: "Cancel This Ride? / Your driver has been notified and may already be on the way." Trigger condition (preimageShared || pinVerified) unchanged —pinVerifiedstill surfaces the dialog for fiat rides, just with honest language. Closes Cancel warning incorrectly mentions payment claiming for fiat/RoadFlare rides #59.Follow-up observations
None filed as GitHub issues yet — noting here so they're not lost:
OfferInbox,RoadflareFollowerList,FollowedDriverList). Options: addkotlinx.collections.immutabledependency, or define@Immutablewrapper data classes. Gate on whether the Compose profiler shows measurable cost from those parents' recompositions after this PR lands — if the leaf ticker + innerrememberblocks are the dominant cost, the collection-stability work may not be needed.Closes
Test plan
./gradlew :common:testDebugUnitTestpasses (addsSatsPlaceholderTest)./gradlew :rider-app:assembleDebug :drivestr:assembleDebug :roadflare-rider:assembleDebug— all three apps assemble clean— satsplaceholder on a cold start (before BTC price fetch), not a$X.XXstring🤖 Generated with Claude Code