Skip to content

Prevent stale purchase data on refresh of Customer Center#3260

Closed
vegaro wants to merge 6 commits intomainfrom
cesar/fix-stale-purchase-detail-normalized
Closed

Prevent stale purchase data on refresh of Customer Center#3260
vegaro wants to merge 6 commits intomainfrom
cesar/fix-stale-purchase-detail-normalized

Conversation

@vegaro
Copy link
Copy Markdown
Member

@vegaro vegaro commented Mar 19, 2026

Summary

  • Fixes stale purchase data displayed on the Customer Center detail screen after a background refresh
  • Normalizes state: navigation destinations now hold a lightweight PurchaseKey (product ID or title+store+isSubscription) 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
  • When a purchase disappears after refresh, preservingUIStateIfRefresh detects the missing key and resets navigation to the main screen
  • No back-stack reconciliation needed — eliminates the reconcileWithPurchases / findMatch pattern

Alternative to facundo/fix-stale-purchase-detail-on-refresh

This is a cleaner alternative to #3252 (or the equivalent branch). Instead of walking the back stack to patch stale PurchaseInformation copies 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 for PurchaseKey matching, 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 PurchaseInformation snapshots and instead storing a lightweight PurchaseKey derived from a new PurchaseInformation.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 cover PurchaseKey generation and findByKey behavior, 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.

…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>
@vegaro vegaro requested a review from a team as a code owner March 19, 2026 11:41
@vegaro vegaro changed the title fix(customer-center): prevent stale purchase data on refresh Prevent stale purchase data on refresh of Customer Center Mar 19, 2026
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>
@vegaro vegaro marked this pull request as draft March 19, 2026 11:54
vegaro and others added 3 commits March 19, 2026 12:57
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
Copy link
Copy Markdown

codecov Bot commented Mar 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.38%. Comparing base (7a29212) to head (8e521e5).
⚠️ Report is 25 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

## 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>
@vegaro vegaro closed this Mar 31, 2026
@vegaro vegaro deleted the cesar/fix-stale-purchase-detail-normalized branch March 31, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants