Repository navigation
Fix storage, network, billing, observer mode, restore and localization bugs - #498
Merged
Merged
Conversation
…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
force-pushed
the
ir/exciting-ptolemy-7y7pfz
branch
from
November 5, 2026 15:05
ab60d77 to
4512547
Compare
This comment has been minimized.
This comment has been minimized.
- 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>
This comment has been minimized.
This comment has been minimized.
… 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>
This comment has been minimized.
This comment has been minimized.
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🟢 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. 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.
Fixed since the last review
|
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.
Changes in this pull request
Storage
reset()and user switches no longer delete every top-level file in the host app'scacheDir. A newCache.clearUserScope()tombstones user-scoped data in memory and deletes our ownUSER_SPECIFIC_DOCUMENTSfolder on the serial write queue, so the new user's writes are kept.LatestConfigandLatestEnrichmentare kept.FullResetnow wipes storage before minting the new alias and seed. The old order would have erased them;Identifyalready used the new order.AtomicFile. The memory cache is aConcurrentHashMap(the LRU classes are removed), and deletes leave a tombstone so a read can't reload a file whose deletion is still queued.Network
NET_CAPABILITY_INTERNETcounts, including Ethernet, VPN and captive portals;VALIDATEDisn't required. The wait usesregisterDefaultNetworkCallback, falls back to polling on API 23, and gives up after 30 seconds.Billing
GoogleBillingWrapperinstance and is cleared on reset. Missing ids are skipped for 5 minutes instead of being cached as failures, and partial results are always returned.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 newisPendingflag keeps it out of the entitlement maths. The deadlatestPurchase()is removed.GoogleBillingWrapperis the only BillingClient. It gainslaunchBillingFlow(returns that flow's result),acknowledgeandqueryPurchasesOfType.AutomaticPurchaseControllertakesBillingin its constructor and no longer creates its own client or reconnect loop.PurchaseResult.Purchasedcarries the Play purchase internally, soTransactionManagerno longer waits on the StateFlow side channel in the default path.Observer mode
ConcurrentHashMapis replaced by a singleAtomicReferenceslot. A second purchase of a different product is rejected and nothing throws. Both documented paths (the collector behindlaunchBillingFlowWithSuperwall, and manualobserve*calls) are kept; whichever reports first claims the slot, so each purchase is handled once.Paywall activity
Options and localization
PaywallOptions.useCachedTemplatesis@Deprecatedand its KDoc is fixed.PaywallComposable's default error are localized in all 40 shipped locales. Text the app sets onrestoreFailedis shown unchanged.PaywallComposablereloads whenplacementchanges, 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_pendingdefaults tofalsefor 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
PaywallRestoreFailedevent; and the PurchaseFlow actor and Store port refactors are not started.Needs review: the translations were written without a native-speaker review.
superwall-composegains Robolectric, MockK and Compose UI-test as test-only dependencies.Checklist
:superwall1,722 tests, plus thesuperwall-composetests.)CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.ktlintin the main directory and fixed any issues. (ktlintisn't configured in the build.)useCachedTemplatesare outside this repo.)Generated by Claude Code