Skip to content

Fix storage, network, billing, observer mode, restore and localization bugs - #498

Merged
ianrumac merged 7 commits into
developfrom
ir/exciting-ptolemy-7y7pfz
Nov 5, 2026
Merged

ianrumac merged 7 commits into
developfrom
ir/exciting-ptolemy-7y7pfz

Conversation

@ianrumac

@ianrumac ianrumac commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Changes in this pull request

Storage

  • reset() and user switches no longer delete every top-level file in the host app's cacheDir. A new Cache.clearUserScope() tombstones user-scoped data in memory and deletes our own USER_SPECIFIC_DOCUMENTS folder on the serial write queue, so the new user's writes are kept. LatestConfig and LatestEnrichment are kept.
  • This also fixes user data carrying over to the next user: confirmed assignments, paywall view counts, customer info, transaction history and the survey key.
  • FullReset now wipes storage before minting the new alias and seed. The old order would have erased them; Identify already used the new order.
  • Writes go through AtomicFile. The memory cache is a ConcurrentHashMap (the LRU classes are removed), and deletes leave a tombstone so a read can't reload a file whose deletion is still queued.

Network

  • Any network with NET_CAPABILITY_INTERNET counts, including Ethernet, VPN and captive portals; VALIDATED isn't required. The wait uses registerDefaultNetworkCallback, falls back to polling on API 23, and gives up after 30 seconds.

Billing

  • The product cache belongs to each GoogleBillingWrapper instance and is cleared on reset. Missing ids are skipped for 5 minutes instead of being cached as failures, and partial results are always returned.
  • Play has no client-side grace period, account hold or expiry, so the device no longer guesses them: expiry is null, PURCHASED means SUBSCRIBED, and "no current purchase but a subscription in history" means EXPIRED. PENDING is no longer reported as revoked or in a grace period; a new isPending flag keeps it out of the entitlement maths. The dead latestPurchase() is removed.
  • GoogleBillingWrapper is the only BillingClient. It gains launchBillingFlow (returns that flow's result), acknowledge and queryPurchasesOfType. AutomaticPurchaseController takes Billing in its constructor and no longer creates its own client or reconnect loop. PurchaseResult.Purchased carries the Play purchase internally, so TransactionManager no longer waits on the StateFlow side channel in the default path.

Observer mode

  • The in-flight ConcurrentHashMap is replaced by a single AtomicReference slot. A second purchase of a different product is rejected and nothing throws. Both documented paths (the collector behind launchBillingFlowWithSuperwall, and manual observe* calls) are kept; whichever reports first claims the slot, so each purchase is handled once.

Paywall activity

  • After process death, the activity finishes (with no animation) instead of showing a blank screen when Superwall isn't configured or the view is gone. A same-process recreation now uses the view registered under its own key instead of swapping in whatever paywall is current.

Options and localization

  • PaywallOptions.useCachedTemplates is @Deprecated and its KDoc is fixed.
  • The restore-failed alert and PaywallComposable's default error are localized in all 40 shipped locales. Text the app sets on restoreFailed is shown unchanged.
  • PaywallComposable reloads when placement changes, and shows its error instead of throwing when there's no Activity.

Breaking: AutomaticPurchaseController's constructor changed. It's noted in the CHANGELOG.

Migration: none needed. File names and format are unchanged, and is_pending defaults to false for older history entries.

Not in this PR: reads still do disk I/O on the caller's thread the first time a key is read; a write that's queued but hasn't run is still lost if the app is killed; there's no PaywallRestoreFailed event; and the PurchaseFlow actor and Store port refactors are not started.

Needs review: the translations were written without a native-speaker review. superwall-compose gains Robolectric, MockK and Compose UI-test as test-only dependencies.

Checklist

  • All unit tests pass. (:superwall 1,722 tests, plus the superwall-compose tests.)
  • All UI tests pass. (Not run: no emulator.)
  • Demo project builds and runs. (Not run.)
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run ktlint in the main directory and fixed any issues. (ktlint isn't configured in the build.)
  • I have updated the SDK documentation as well as the online docs. (The online docs for useCachedTemplates are outside this repo.)
  • I have reviewed the contributing guide

Generated by Claude Code

…n bugs

- Cache: user-scope reset via tombstones instead of wiping the host's
  cacheDir; AtomicFile writes; thread-safe memory cache; tombstoned deletes.
- Reset wipes storage before minting the new identity.
- Network wait accepts any transport with INTERNET, uses the default
  network callback and gives up after 30s.
- Product cache is per wrapper instance, never caches misses as errors,
  retries missing ids after 5 minutes, clears on reset.
- Device receipts no longer guess expiry/grace; PURCHASED is SUBSCRIBED,
  PENDING is pending (not revoked). Removes dead latestPurchase().
- Observer mode uses a single in-flight slot, handled once by either path.
- GoogleBillingWrapper is the only BillingClient; AutomaticPurchaseController
  launches flows, acknowledges and queries through it, and the purchase
  result carries the Play purchase.
- Paywall activity finishes when recreated without its paywall.
- Deprecates PaywallOptions.useCachedTemplates.
- Localizes the restore-failed alert and PaywallComposable's error; the
  composable reloads on placement change and no longer throws without an
  Activity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gApsMVje34EFAktGWwR
- A failed Play read is no longer treated as "no purchases": receipts and
  customer info aren't overwritten and latestSubscriptionState isn't set to
  EXPIRED. Billing gains queryAllPurchasesResult(), which waits for the
  connection and fails instead of returning an empty list.
- Play subscribers stay active through a failed read until a hold date
  (last confirmed + one subscription period), stored separately from the
  user-facing expiry, which stays unknown.
- A new observed purchase replaces one that never reported an outcome and
  tracks it as abandoned, so observer mode can't get stuck.
- After reset/user switch, the new user's receipts are rebuilt from Play.
- Cache: memory updates and queued disk work happen under one lock, and a
  disk read that races clearUserScope doesn't resurrect the old value.
- The billing flow is launched on the main thread.
- CacheInstrumentedTest uses clearUserScope(); the PaywallComposable
  placement test now fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gApsMVje34EFAktGWwR
@ianrumac
ianrumac force-pushed the ir/exciting-ptolemy-7y7pfz branch from ab60d77 to 4512547 Compare November 5, 2026 15:05
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 inline notes from Maple's review. The score and summary are in the review comment above.

ianrumac and others added 2 commits November 5, 2026 16:44
- Restore gradle versions, wrapper and superwall/build.gradle.kts the IDE downgraded
- Keep restore-failed text the app sets explicitly, even when it equals the English default
- Leave pending purchases out of published purchase history; drop cancelled pending purchases
- Drop expiry dates older versions inferred for stored Play transactions
- Fix GoogleBillingWrapper companion parsed as a companion named 'internal'
- Update API dump and tests for the new billing semantics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both sides in StoreManager and ReceiptManager. Keep this branch's null
Play expiry: #506 already stopped guessing expiry for an unknown base plan,
and purchaseTime + period is also wrong after renewals. Update #506's tests
for the Result-based purchase read and the null expiry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

github-actions Bot and others added 2 commits November 5, 2026 16:09
… in restore tests

- F4: a cancelled product lookup no longer computes and publishes a state
- androidTest PaywallView mock provides a context for the localized restore-failed alert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

AtomicFile.openRead() deletes the .new file whenever the base exists, which
drops a write the ioQueue is still making to the same key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maple-review-bot

maple-review-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge. Regression tests cover the changed behavior, and the overlapping-write fix removes the destructive read without changing persisted formats.
no findings · tests covered · risk high −1 · 0/1 new units observable −1

Fixes user-scoped storage resets, billing and observer flows, network waits, activity restoration, and localization. The direct cache-file read resolves F5 without disturbing an in-progress atomic write; no new merge-blocking defects were found.

  • Cache.clearUserScope preserves app data and serializes user-data deletion.
  • AutomaticPurchaseController shares the SDK billing client.
  • Receipt processing excludes pending purchases from entitlement calculations.
  • PaywallComposable reloads changed placements and uses localized errors.

Fixed since the last review

  • ✅ F5 · readFromDisk can discard an overlapping atomic write

063ca90 · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@ianrumac
ianrumac merged commit 6eec155 into develop Nov 5, 2026
13 checks passed
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.

2 participants