Prevent stale purchase data on refresh of Customer Center#3260
Closed
Prevent stale purchase data on refresh of Customer Center#3260
Conversation
…rmalized state Navigation destinations now hold a lightweight PurchaseKey instead of the full PurchaseInformation object. The canonical purchases list in Success state is the single source of truth — the UI resolves fresh data at render time via selectedPurchase/promotionalOfferPurchase derived properties. This eliminates the need for back-stack reconciliation on refresh. When a purchase disappears after refresh, preservingUIStateIfRefresh detects the missing key and resets navigation to the main screen. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Product ID alone isn't unique — a user can have multiple purchases of the same product (e.g. non-subscription consumables). Thread the store transaction ID through TransactionDetails → PurchaseInformation and prefer it as the primary key (PurchaseKey.ByStableId). Falls back to ByProductId then ByAttributes when the transaction ID isn't available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every purchase has a productIdentifier regardless of store, so ByAttributes (title+store+isSubscription) is unnecessary. The key priority is now: 1. ByStableId — store transaction ID (unique per purchase) 2. ByProductId — product identifier (unique for subscriptions, available for all stores) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the sealed PurchaseKey class (ByStableId/ByProductId) with a simple data class using productIdentifier + purchaseDate. Both fields are always available from SubscriptionInfo and Transaction, and together they uniquely identify a purchase even when multiple purchases share the same product ID (e.g. consumables). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PurchaseKey now uses storeTransactionId + productIdentifier + purchaseDate. This handles even the edge case where two purchases of the same product happen at the exact same timestamp — the transaction ID disambiguates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3260 +/- ##
=======================================
Coverage 79.38% 79.38%
=======================================
Files 357 357
Lines 14347 14347
Branches 1959 1959
=======================================
Hits 11389 11389
Misses 2154 2154
Partials 804 804 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
3 tasks
1 task
## Summary - Add ViewModel-level integration tests that verify the normalized navigation state (PurchaseKey) works correctly during refresh scenarios - Covers the test gap identified when comparing #3260 against #3252 ### Tests added 1. **`refresh keeps detail screen and resolves fresh purchase data via PurchaseKey`** — selects a purchase, triggers a refresh with updated subscription data, verifies the detail screen persists and `selectedPurchase` resolves fresh data from the canonical list 2. **`refresh pops to main when selected purchase no longer exists`** — selects a purchase, refreshes with an empty subscription list, verifies navigation resets to Main with CLOSE button 3. **`refresh on main screen preserves navigation state`** — verifies refresh while on the main screen doesn't break navigation ## Test plan - [x] `./gradlew :ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests "*CustomerCenterViewModelTests"` Follow-up to #3260 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Adds ViewModel-level unit tests only; no production code paths change, so risk is limited to test stability/flakiness around coroutine timing and mocked refresh behavior. > > **Overview** > Adds **refresh reconciliation** coverage to `CustomerCenterViewModelTests` to validate the `PurchaseKey`-based navigation model during `refreshCustomerCenter()`. > > The new tests assert that refreshing while on a purchase detail screen keeps the detail destination and re-resolves `selectedPurchase` from freshly fetched purchase data, and that refresh correctly **pops back to Main** when the selected purchase disappears. It also verifies refresh on the Main screen preserves the current navigation state. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8ff8487. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 (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.
Summary
PurchaseKey(product ID or title+store+isSubscription) instead of the fullPurchaseInformationobjectpurchaseslist inSuccessstate is the single source of truth — the UI resolves fresh data at render time viaselectedPurchase/promotionalOfferPurchasederived propertiespreservingUIStateIfRefreshdetects the missing key and resets navigation to the main screenreconcileWithPurchases/findMatchpatternAlternative to
facundo/fix-stale-purchase-detail-on-refreshThis is a cleaner alternative to #3252 (or the equivalent branch). Instead of walking the back stack to patch stale
PurchaseInformationcopies after every refresh, this approach prevents staleness structurally by not storing mutable domain data in navigation state.Note
Medium Risk
Navigation state and refresh behavior were refactored to key purchases by a stable identifier instead of storing
PurchaseInformation, which could affect detail/promotional-offer routing and back stack behavior during refreshes. Risk is moderated by added unit tests forPurchaseKeymatching, but edge cases around missing/duplicate identifiers could still surface UI regressions.Overview
Prevents stale purchase data in Customer Center by stopping navigation destinations from carrying
PurchaseInformationsnapshots and instead storing a lightweightPurchaseKeyderived from a newPurchaseInformation.stableId(fallbacks to product ID or attributes).The UI now resolves the active purchase at render/action time via
CustomerCenterState.Success.selectedPurchase/promotionalOfferPurchase, and refresh logic (preservingUIStateIfRefresh) validates that the keyed purchase still exists—resetting to main when it doesn’t and recomputing detail screen paths when needed. Unit tests were updated/added to coverPurchaseKeygeneration andfindByKeybehavior, and navigation tests were adjusted for the new destination payloads.Written by Cursor Bugbot for commit 5314460. This will update automatically on new commits. Configure here.