SIGN IN SIGN UP

Clear paywall web view storage when the user logs out or switches (#4075)

- Clears the paywall WebView profile's browsing data when an identified
user leaves: `logOut`, `switchUser`, and `logIn` away from an already
identified user.
- Keeps the data when an anonymous user signs in. That transition is the
middle of a multipage paywall or workflow flow, so the storage belongs
to the same customer and dropping it would break the flow they are in.
- Headline caveat: on System WebViews without `DELETE_BROWSING_DATA` the
fallback clears cookies and `WebStorage.deleteAllData` only, so that
profile's network cache, IndexedDB and CacheStorage survive. Those
devices get less than the guarantee the first bullet implies.
- Goes further than iOS on one path: `purchases-ios#7470` clears on
`logOut` and the customEntitlementComputation `switchUser`, so
`logIn(A)` then `logIn(B)` keeps A's data there. This clears it.
- Resets `PaywallWebViewPrewarmer`'s warmed set after a successful
clear, since the deleted data includes the network cache that set is a
memo of.

### Checklist
- [x] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation


[PWENG-221](https://linear.app/revenuecat/issue/PWENG-221/android-clear-the-paywall-webview-profile-on-identity-change).
Paywall WebViews have had an isolated `com.revenuecat.paywall` profile
since #3816, but nothing ever cleared it: no `CookieManager`,
`WebStorage` or `ServiceWorkerController` reference existed anywhere in
the repo. So one user's paywall cookies and DOM storage survived into
the next user's session. Isolated from the host app, not isolated
between users.

The policy is narrower than "on identity change", and comes from [Jake's
comment on
purchases-ios#7470](https://github.com/RevenueCat/purchases-ios/pull/7470#discussion_r3847188423):
clear on log out or on a switch from user A to user B, not on anonymous
to known, because most multipage flows are an anonymous customer
starting a flow, getting identified, then continuing it. Clearing there
would destroy the warm cache mid-conversion.

### Description

- Adds `clearWebViewStorage(context)` to the existing
`PaywallAssetWarmer` `ServiceLoader` SPI, which is the only push channel
from `:purchases` to `:ui:revenuecatui` and already carries the "return
promptly, do not throw" contract with `runCatching` isolation on the
core side.
- `clearPaywallProfileStorage()` calls
`WebStorageCompat.deleteBrowsingData` on `Profile.getWebStorage()`.
Scoped to our profile, so the host app's `Default` profile is untouched.
- Hooks two sites in `IdentityManager`: `logIn`'s backend-success block,
and `resetAndSaveUserID`, which is the sole funnel for `logOut` and
`switchUser` including the customEntitlementComputation flavor's public
`switchUser`.
- Guard is `oldAppUserID != newAppUserID &&
!isUserIDAnonymous(oldAppUserID)`, so the helper owns the whole rule
rather than depending on `PurchasesOrchestrator`'s same-id guards
upstream.
- `getProfile`, not `getOrCreateProfile`: an app that never rendered a
`web_view` paywall has nothing to clear, and should not get a profile
created on disk in order to wipe it.

Not visible in the diff:

- `deleteBrowsingData` is documented as deleting "network cache,
cookies, and any JavaScript-readable storage", which is the same outcome
iOS gets from `WKWebsiteDataStore.remove(forIdentifier:)`. Verified
against the `androidx.webkit` 1.16.0 sources, not the docs site.
- `DELETE_BROWSING_DATA` is negotiated separately from `MULTI_PROFILE`,
so a device can have the isolated profile without the delete API. Hence
the fallback branch rather than a single call.
- androidx documents the deletion as non-atomic: it "may also delete
data that is stored during deletion". That is why the prewarmer reset
runs in the done callback instead of inline, so a re-warm cannot start
into a deletion that is still running.
- Everything touched is `@InternalRevenueCatAPI` or internal, and those
members are excluded from the metalava dumps, so `api-*.txt` is
unchanged.
- The reset is wired into the `deleteBrowsingData` branch only. The
fallback does not touch the network cache, so `warmedUrls` stays
accurate there.

### Regression gates

- `login from an anonymous user keeps the paywall web view storage` in
`IdentityManagerTests`: the test that fails if the anonymity exemption
is ever dropped, which is the whole policy.
- `falls back to cookies and web storage when deleting browsing data is
unsupported` in `WebViewProfileTest`, for the older-WebView branch.
- `warms a url again once the cache it was warmed into is cleared` in
`PaywallWebViewPrewarmerTest`, for the stale-memo bug.

**Limitations:**

- Pre-113 System WebViews have no isolated profile at all: paywalls fall
back to the host app's `Default` profile, which is not ours to wipe, so
nothing is cleared there. Logged at debug.
- Clearing runs even with a paywall on screen, so a live page loses its
cookies underneath it. Logging out with a paywall visible is not a flow
we support, and the alternative costs the store-rotation machinery
below.
- The first qualifying identity change in a process can pay the WebView
provider load on a main-thread frame, since
`WebViewFeature.isFeatureSupported` is what triggers that load. It lands
even on apps whose paywalls contain no `web_view` component, which get
no benefit from the clear. Gating on "this process already touched the
paywall WebView" would skip it but would be wrong: the profile is
persistent, so a previous process's data has to be clearable.
- `IdentityManager.configure()` with an explicit app user ID that
differs from the cached one still clears nothing. It never captures the
old id, so no cache is dropped on that path today, not just this one.
Worth its own issue; it is the same leak for an app that manages sign-in
by reconfiguring.

**Rejected:**

- `ProfileStore.deleteProfile`: throws both for a profile any WebView is
live on and for one that `getOrCreateProfile` has loaded, and
`applyPaywallProfile` always loads it. Unusable here.
- Rotating profile names with a delete-on-next-start sweep, which is
what iOS does with data store identifiers. Needs a persisted generation
counter plus a startup sweep, and `deleteBrowsingData` reaches the same
end state in one call.
- `WebView.clearCache(true)`: clears the host app's WebView cache too,
so it breaks the isolation the profile exists for.
- Clearing only cookies and DOM storage to preserve the prewarmed
bundles. `WebStorage.deleteAllData` documents only Web SQL and Web
Storage, so the network cache, IndexedDB and CacheStorage would keep the
previous user's data. The bundle is server-authored, so which stores it
uses is not ours to assume.
- Hooking `PurchasesOrchestrator` instead: three call sites rather than
two, and a future identity path added inside `IdentityManager` would
silently miss the clear.

</details>
Á
Álvaro Brey committed
c673fb768635e56d51968802c091276dc042ce18
Parent: 0a50b9a
Committed by GitHub <noreply@github.com> on 8/28/2026, 4:14:23 PM