Skip to content

Add container API: InAppWebViewSettings.containerId, ContainerController, per-container proxy#2825

Open
theoden8 wants to merge 11 commits into
pichillilorenzo:masterfrom
theoden8:claude/improve-inappwebview-isolation-2mUko
Open

Add container API: InAppWebViewSettings.containerId, ContainerController, per-container proxy#2825
theoden8 wants to merge 11 commits into
pichillilorenzo:masterfrom
theoden8:claude/improve-inappwebview-isolation-2mUko

Conversation

@theoden8

@theoden8 theoden8 commented Apr 28, 2026

Copy link
Copy Markdown

Testing and Review Notes

What's new: InAppWebViewSettings.containerId lets a WebView join a named, persistent storage container (cookies + DOM storage + IndexedDB + ServiceWorkers + HTTP cache). InAppWebViewSettings.proxySettings attaches a proxy to that container. ContainerController enumerates and deletes them. Naming follows Firefox's Multi-Account Containers; native APIs (androidx.webkit.Profile, WKWebsiteDataStore(forIdentifier:)) keep their platform names in the implementation packages.

Platform support: Android (System WebView 110+), iOS 17+, macOS 14+, Linux. Older versions and Windows/Web silently ignore the new settings; ContainerController returns empty/false on unsupported platforms. Gate UI with InAppWebViewSettings.isPropertySupported(InAppWebViewSettingsProperty.containerId) and ContainerController.isClassSupported().

How to test:

cd flutter_inappwebview/example
NODE_SERVER_IP=<ip> flutter test integration_test/in_app_webview/container_isolation.dart

The integration test asserts that two WebViews with different containerIds don't share localStorage, that re-joining a container restores its data, and that ContainerController.getAllContainerNames / hasContainer / deleteContainer round-trip. End-to-end validation against a multi-account browser consumer at theoden8/webspace_app#250, theoden8/webspace_app#255 and theoden8/webspace_app#256.

Implementation notes:

  • Container join is fixed at WebView construction. Android pins it the moment addJavascriptInterface / setAcceptThirdPartyCookies first run; Apple copies WKWebViewConfiguration.websiteDataStore by value at WKWebView.init. So the join is wired in prepare() (Android) and preWKWebViewConfiguration (Apple). setSettings after construction is silently ignored — the WebView stays in whichever container it joined at birth.
  • Apple's API takes UUIDs, not strings. containerId is hashed (SHA-256, first 16 bytes) into a stable UUID; an id ↔ UUID side-map in a UserDefaults suite lets getAllContainerNames return the original strings, intersected with fetchAllDataStoreIdentifiers so stale entries are filtered out.
  • MyCookieManager (all three native platforms) now resolves the per-container cookie store via an optional webViewId that Dart's cookie_manager forwards when a webViewController is supplied. Unscoped callers stay on the default-jar path — byte-identical to current behaviour.

Relation to #2671: that PR routed iOS proxy through the global default() / nonPersistent() stores; this PR's proxySettings attaches to whichever store the WebView ended up with (typically the per-container one), so proxy isolation tracks container membership instead of being global. Similar on Linux as it uses webkit port also.

CHANGELOGs updated in all five touched packages.

Screenshots or Videos

N/A — non-visual. Happy to record the integration test or a two-tab login demo on request.

@probot-autolabeler probot-autolabeler Bot added android documentation iOS linux Linux platform macOS macOS platform platform_interface Platform Interface plugin flutter_inappwebview plugin web windows Windows platform labels Apr 28, 2026
claude added 3 commits April 28, 2026 15:47
…Settings for per-WebView data store partitioning

Adds two opt-in, nullable fields to the source InAppWebViewSettings_
class and regenerates in_app_webview_settings.g.dart. Native bindings
land in subsequent platform commits.

* containerId: a free-form String identifier for a persistent,
  isolated data partition. Naming follows Firefox's "Multi-Account
  Containers" convention, which predates and reads more clearly than
  the underlying platform terms (Android calls them "profiles", Apple
  calls them "data store identifiers"). Tagged @SupportedPlatforms
  for Android (any version with WebViewFeature.MULTI_PROFILE), iOS
  17+ and macOS 14+.

* proxySettings: a ProxySettings (reuses the existing global-proxy
  type) scoped to a single WebView's data store. Tagged
  @SupportedPlatforms for iOS 17+ / macOS 14+ only — there is no
  per-WebView proxy API on the other platforms.

Both fields are nullable and default to null; toMap/fromMap
round-trips the new entries; existing callers see no behavior change.
…g and deleting per-WebView containers

Defines a new abstract PlatformContainerController exposing three
methods that map to the underlying native APIs:

  Future<List<String>> getAllContainerNames();
  Future<bool>         hasContainer(String containerId);
  Future<bool>         deleteContainer(String containerId);

@SupportedPlatforms is set for AndroidPlatform, IOSPlatform(17.0) and
MacOSPlatform(14.0) — the same platforms that honor
InAppWebViewSettings.containerId. Linux / Windows / Web stay
UnimplementedError-by-default.

The class follows the same factory + creation-params + .static()
pattern as PlatformProxyController and PlatformWebStorageManager. Two
new factory hooks on InAppWebViewPlatform
(createPlatformContainerController and
createPlatformContainerControllerStatic) let federated packages
register their concrete implementations. The exchangeable codegen
produces platform_container_controller.g.dart with the
isClassSupported / isMethodSupported helpers.

Naming note: "container" mirrors Firefox's Multi-Account Containers
terminology, which predates and reads more clearly than the
underlying platform terms (Android calls them "profiles", Apple calls
them "data store identifiers"). Native API references in the
implementation packages keep the platform names as Google / Apple
publish them.
…ller wrapping androidx.webkit.ProfileStore

We map "container" (our public concept, naming follows Firefox) to
androidx.webkit.Profile (Android's underlying primitive). The native
API names — Profile, ProfileStore, WebViewFeature.MULTI_PROFILE,
WebViewCompat.setProfile/getProfile — stay as Google publishes them.

Pieces:

* InAppWebViewSettings.java parses/serializes the new "containerId"
  key.

* InAppWebView.prepare() binds the WebView to the named container via
  ProfileStore.getOrCreateProfile + WebViewCompat.setProfile at the
  very top of prepare(), before addJavascriptInterface,
  addDocumentStartJavaScript and CookieManager.setAcceptThirdPartyCookies
  run — those calls lock the WebView to whichever profile is current,
  and there is no API to rebind a live WebView. Guarded by
  WebViewFeature.isFeatureSupported(MULTI_PROFILE) and try/catch — on
  System WebView <110 the WebView falls back to the default profile.

* MyCookieManager gains a webViewId-scoped path
  (cookieManagerForContainerOf) so cookie ops on a container-bound
  WebView land in the container's cookie jar rather than the global
  default. The store is keyed by container, not by WebView — two
  WebViews sharing a containerId share their cookies. Falls back to
  the global manager when the lookup misses.

* InAppWebView gains a small live-instance registry (weak refs in a
  ConcurrentHashMap) so the cookie manager can find a WebView by its
  platform-view id reliably even when the view is detached from the
  visible tree.

* New native file container/ContainerManager.java handles a fresh
  com.pichillilorenzo/flutter_inappwebview_containercontroller
  MethodChannel. Methods:
    - getAllContainerNames → ProfileStore.getInstance().getAllProfileNames()
    - hasContainer(name)   → name in getAllProfileNames()
    - deleteContainer(name)→ ProfileStore.getInstance().deleteProfile(name)
  Every method short-circuits to empty / false when MULTI_PROFILE is
  unsupported, so callers get a consistent answer on System WebView
  <110 instead of an exception. The default profile name is rejected
  up front; IllegalStateException from "profile in use by a live
  WebView" is surfaced as success(false) — the contract of
  deleteContainer is "true when it actually deleted something".

* AndroidContainerController (lib/src/container_controller.dart) is
  the concrete PlatformContainerController. Same shape as
  AndroidProxyController.

* InAppWebViewFlutterPlugin.java instantiates ContainerManager in
  onAttachedToEngine and disposes it on detach.

* AndroidInAppWebViewPlatform.createPlatformContainerController(Static)
  factories return AndroidContainerController.

* Dart-side cookie_manager.dart forwards webViewController.id to the
  native channel when a controller is supplied.
@theoden8 theoden8 force-pushed the claude/improve-inappwebview-isolation-2mUko branch from caeaad1 to 26606bf Compare April 28, 2026 15:51
@theoden8 theoden8 changed the title Per-profile isolation: InAppWebViewSettings.profileId + per-profile proxy Add container API: InAppWebViewSettings.containerId, ContainerController, per-container proxy Apr 28, 2026
claude added 4 commits April 29, 2026 17:23
… WKWebsiteDataStore + UserDefaults registry

iOS 17+ only. We map "container" (our public concept, naming follows
Firefox) to WKWebsiteDataStore identifier-based stores (Apple's
underlying primitive).

Apple's API stores data store identifiers as UUIDs, while our
containerId is a free-form String — and SHA-256 is one-way, so we
can't recover the original String from a UUID. ContainerManager
maintains an `containerId → uuidString` side-map in a UserDefaults
suite ("com.pichillilorenzo.flutter_inappwebview.containers"),
updated automatically when a containerId is bound (via
InAppWebView.preWKWebViewConfiguration's call to
ContainerManager.registerContainerBinding) and when a container is
deleted through this controller. The map survives app restarts;
uninstall wipes it together with the underlying WKWebsiteDataStore
data, so they stay consistent.

Pieces:

* InAppWebViewSettings.swift declares containerId and proxySettings.
  proxySettings is carried as the raw Dart map and converted via the
  existing ProxySettings.fromMap inside iOS-17 availability blocks.

* InAppWebView.preWKWebViewConfiguration sets
  configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier:)
  when containerId is supplied and #available(iOS 17.0, *). The
  identifier is derived via SHA-256 of the UTF-8 bytes (first 16
  bytes formatted as a UUID). incognito always wins. The same block
  attaches proxySettings to the data store the WebView ended up with,
  so a container-bound WebView genuinely uses its own proxy.

* MyCookieManager gains a webViewId-scoped path
  (httpCookieStore(forProfileOf:)) so cookie ops land in the bound
  container's WKHTTPCookieStore. The store is keyed by container, not
  by WebView; two WebViews sharing a containerId share their cookies.

* InAppWebView gains a live-instance registry (NSHashTable.weakObjects)
  so the cookie manager can find a WebView by its platform-view id
  reliably, including when detached from the visible hierarchy.

* New native file ContainerManager.swift handles a fresh
  com.pichillilorenzo/flutter_inappwebview_containercontroller
  MethodChannel. Methods:
    - getAllContainerNames intersects the UserDefaults registry with
      WKWebsiteDataStore.fetchAllDataStoreIdentifiers, so stale
      registry entries (where the underlying store has been removed
      out of band) are filtered out automatically.
    - hasContainer derives the UUID via containerIdToUUID and checks
      it against the live identifier list.
    - deleteContainer calls WKWebsiteDataStore.remove(forIdentifier:),
      which returns an error when no such store exists or when a live
      WKWebView is using it; both are surfaced as success(false).
      Removes the registry entry on success.

* IOSContainerController is the concrete PlatformContainerController.

* InAppWebViewFlutterPlugin.swift instantiates ContainerManager under
  #available(iOS 17.0, *).

* IOSInAppWebViewPlatform.createPlatformContainerController(Static)
  factories return IOSContainerController.

* Dart-side cookie_manager.dart forwards webViewController.id to the
  native channel when a controller is supplied.
… via WKWebsiteDataStore + UserDefaults registry

Mirror of the iOS implementation, gated on macOS 14+ for the
identifier-based WKWebsiteDataStore APIs (Apple ships them
simultaneously across iOS 17 and macOS 14). The SHA-256 → UUID
derivation is byte-identical to the iOS sibling so the same
containerId maps to the same on-disk store on either platform —
useful when a user's data syncs across devices.

Files mirror the iOS commit:
  - InAppWebViewSettings.swift: containerId, proxySettings declarations
  - InAppWebView.swift: liveWebViews registry, containerIdToUUID helper,
    websiteDataStore + proxyConfigurations bind in
    preWKWebViewConfiguration(settings:), call to
    ContainerManager.registerContainerBinding
  - MyCookieManager.swift: httpCookieStore(forProfileOf:) helper plus
    webViewId-scoped getCookies/deleteCookie overloads
  - ContainerManager.swift: WKWebsiteDataStore.fetchAllDataStoreIdentifiers /
    .remove(forIdentifier:) with the UserDefaults id ↔ UUID side-map
  - lib/src/container_controller.dart: MacOSContainerController
  - lib/src/inappwebview_platform.dart: factory hooks
  - InAppWebViewFlutterPlugin.swift: instantiates ContainerManager under
    #available(macOS 14.0, *)
  - lib/src/cookie_manager.dart: forwards webViewController.id
…entries, container_isolation integration test

Public-facing wrapper class and tests that pull together the
per-platform PlatformContainerController implementations from prior
commits.

* lib/src/container_controller.dart wraps PlatformContainerController
  with the same shape as ProxyController. ContainerController
  delegates to the platform implementation registered by the chosen
  federated package, exposes static isClassSupported /
  isMethodSupported, and is reachable via ContainerController.instance().

* CHANGELOG entries land in:
  - flutter_inappwebview_platform_interface (1.4.0-beta.3)
  - flutter_inappwebview (6.2.0-beta.3, with per-platform sub-sections
    matching the existing 6.2.0-beta.3 style)

* example/integration_test/in_app_webview/container_isolation.dart
  is a new integration test that asserts:
   - two WebViews bound to different containerIds do not share
     localStorage state;
   - a WebView re-bound to the same containerId reads back its
     previously-written marker;
   - ContainerController list/has/delete round-trips a probe
     container (materialized via a HeadlessInAppWebView and disposed
     before deleting, since deleteContainer fails while a live
     WebView is using the container on every supported platform).
  Skipped when InAppWebViewSettings.isPropertySupported(containerId)
  or ContainerController.isClassSupported() returns false, so it
  no-ops on platforms without per-container partitioning.
…nAppWebViewSettings.containerId

LinuxPlatform() joins AndroidPlatform / IOSPlatform / MacOSPlatform on
the @SupportedPlatforms blocks for InAppWebViewSettings.containerId
and on the PlatformContainerController class plus its three methods
(getAllContainerNames, hasContainer, deleteContainer).

The Linux notes describe the filesystem-backed shape rather than
referencing a native API class — WPE WebKit's WebKitNetworkSession
takes data + cache directories directly, so the controller scans
under <XDG_DATA_HOME>/flutter_inappwebview/containers/ for
getAllContainerNames and removes both the data and cache subtrees
on deleteContainer.

While here, fixed three sed-mangled native API references in this
file from the original rename pass: ProfileStore.getAllProfileNames,
ProfileStore.deleteProfile, and the corresponding URL anchors should
match the literal androidx.webkit names — those are Google's, not
ours to rename.

Regenerates platform_container_controller.g.dart and
in_app_webview_settings.g.dart via build_runner; the generated diff
adds TargetPlatform.linux to the support tables.
@theoden8 theoden8 force-pushed the claude/improve-inappwebview-isolation-2mUko branch from 9723392 to d1994d7 Compare April 29, 2026 17:23
claude added 2 commits April 29, 2026 19:53
… WebKitNetworkSession

Adds WebView container support on Linux WPE WebKit, matching the API
shape already implemented for Android, iOS and macOS:

* InAppWebViewSettings.containerId is parsed from the Dart map and,
  when non-empty (and incognito is false), causes InitWebView to
  create or reuse a process-wide cached WebKitNetworkSession with
  per-container data and cache directories under
    <XDG_DATA_HOME>/flutter_inappwebview/containers/<id>/data
    <XDG_CACHE_HOME>/flutter_inappwebview/containers/<id>/cache
  Cached sessions hold an extra ref so they survive a WebView
  teardown — without that, lazy-reloading a WebView into the same
  container aborts with a thread-join error inside libwebkit. Both
  HAVE_WPE_PLATFORM and HAVE_WPE_BACKEND_LEGACY backend branches
  thread the session through g_object_new's "network-session"
  property, mirroring the existing incognito path.

* New ContainerManager (linux/container_manager.{h,cc}) handles the
  com.pichillilorenzo/flutter_inappwebview_containercontroller
  channel:
    - getAllContainerNames lists subdirectories of the data root
    - hasContainer checks for one such subdirectory
    - deleteContainer removes both the data and cache subtrees and
      returns true iff at least one of them existed
  Registered alongside ProxyManager in
  flutter_inappwebview_linux_plugin.cc and exposed on PluginInstance
  for symmetry with the other managers.

* Dart side: LinuxContainerController extends PlatformContainerController
  and is wired through LinuxInAppWebViewPlatform.createPlatformContainerController(Static).

The container join requires WPE WebKit 2.40+ (where
webkit_network_session_new exists); on older runtimes the network-
session property is unavailable and the WebView falls back to the
shared default session, the same way it already falls back when
incognito is requested but the session creation fails.

CHANGELOG entry under 0.1.0-beta.2; pubspec version bumped to match.
…ry, bump linux dep to ^0.1.0-beta.2

Renames the lingering hasProfile/deleteProfile references in the
Android, iOS, macOS and public CHANGELOG entries to hasContainer /
deleteContainer to match the actual API. Adds a "Linux Platform"
sub-section to the public 6.2.0-beta.3 entry covering
PlatformContainerController and the WebKitNetworkSession-backed
containerId join, and bumps the flutter_inappwebview_linux dep
listed in the dependency-bump bullet to ^0.1.0-beta.2.

No code changes — pure docs.
@theoden8 theoden8 force-pushed the claude/improve-inappwebview-isolation-2mUko branch from d1994d7 to 0abc7f0 Compare April 29, 2026 19:53
claude added 2 commits May 18, 2026 13:45
…e across all platforms

Adds clearContainerData(containerId) to PlatformContainerController.
This is the API the consumer needed when deleteContainer's "must not
be in use" precondition can't be met — a "Clear Site Data" button on
a live WebView. Apple's WKWebsiteDataStore.remove(forIdentifier:)
silently no-ops if any WKWebView still references the store; Android's
ProfileStore.deleteProfile throws IllegalStateException; Linux's
deleteContainer just rms the directories without affecting the
in-memory session. None of those serve the "wipe but keep alive" case.

clearContainerData does. Per platform:

  Apple (iOS 17+ / macOS 14+):
    WKWebsiteDataStore(forIdentifier: uuid).removeData(
      ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
      modifiedSince: .distantPast,
      completionHandler: …)
    Works while a WKWebView is bound. The id ↔ UUID registry is left
    intact — only the store's contents are gone.

  Linux (WPE WebKit 2.40+):
    webkit_website_data_manager_clear(manager, WEBKIT_WEBSITE_DATA_ALL,
                                      0 /* since epoch */, …)
    Routed through the container's cached WebKitNetworkSession (the
    one InitWebView's get_or_create_container_session populates). If
    the container has never been joined this process there's no live
    session to clear — returns false. Callers that need to wipe a
    never-joined container can use deleteContainer.

  Android (androidx.webkit.Profile, MULTI_PROFILE 110+):
    Profile has no single clear-all primitive, so this composes:
      - profile.getCookieManager().removeAllCookies(null)
      - profile.getWebStorage().deleteAllData()
      - profile.getGeolocationPermissions().clearAll()
    Documented as best-effort. The per-WebView HTTP cache (cleared
    via WebView.clearCache) and the *global* ServiceWorkerController
    Compat are NOT reached — these aren't scoped to a Profile.
    Callers that need the HTTP cache wiped should also call
    InAppWebViewController.clearCache on every live WebView in the
    container.

Returns Future<bool>: true when the platform reported success on the
subsystems it touched; false when the container does not exist or
the underlying call errored. Per-platform notes are encoded in the
@SupportedPlatforms annotation on PlatformContainerController.clear
ContainerData so the generated supported-platforms docs are honest
about the asymmetry.

CHANGELOG entries added to the platform_interface, android, ios,
macos, linux and flutter_inappwebview packages.

Sits on top of the existing container API; doesn't change any
existing call signatures. Consumers (e.g. webspace_app) can use this
without touching their containerRev defense-in-depth workaround —
the workaround stays valuable for the case where they want a fresh
store regardless of Apple's release timing.
…g Linux's session cache

Apple's WKWebsiteDataStore(forIdentifier: uuid) hands out a fresh
ObjC wrapper on every call. All wrappers for the same UUID back the
same underlying on-disk store, but the wrappers themselves are
independent objects — each kept alive by whoever's holding it. When
multiple WebViews join the same container, each one's
configuration.websiteDataStore gets a different wrapper for the same
underlying store, and so does every ContainerController op
(clearContainerData, etc.). All of those wrapper instances together
constitute an "in-use" reference graph that WKWebsiteDataStore.
remove(forIdentifier:) has to wait on.

We can't fix the WebView-lifetime side of that race (the consumer's
containerRev defense-in-depth workaround handles that), but we can
stop being one of the wrapper duplicators. Mirrors the same shape
Linux's container_session_cache already uses for the same reason —
the Linux header documents the symptom as
"std::system_error during session teardown thread joins" and Apple
likely has the same wrapper-duplication problem even though the
symptom is the silent no-op of remove(forIdentifier:) rather than an
abort.

ContainerManager.swift (iOS + macOS):
  - Adds `static var sharedStores: [UUID: WKWebsiteDataStore]` with
    an NSLock.
  - Adds `getOrCreateDataStore(forContainer:)` which returns the
    cached wrapper (or creates+caches one) and updates the id ↔ UUID
    registry in the same call.
  - Adds `evictDataStore(forContainer:)` used by deleteContainer to
    drop the cached wrapper before WKWebsiteDataStore.remove
    (forIdentifier:) runs. The WebView-side wrapper is still
    in-use; this just removes the plugin as a reason.
  - clearContainerData now goes through the cache so it operates on
    the *same* wrapper any live WebView is holding via its
    configuration.websiteDataStore.
  - registerContainerBinding kept for backwards compatibility; new
    code paths use getOrCreateDataStore which does both jobs.

InAppWebView.swift (iOS + macOS):
  - The inline WKWebsiteDataStore(forIdentifier: uuid) +
    registerContainerBinding pair in preWKWebViewConfiguration is
    replaced with a single ContainerManager.getOrCreateDataStore
    (forContainer:) call. Sibling WebViews in the same container now
    share one wrapper.

No Dart-side API change. CHANGELOGs updated under the in-progress
1.2.0-beta.3 entries for iOS and macOS.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android documentation iOS linux Linux platform macOS macOS platform platform_interface Platform Interface plugin flutter_inappwebview plugin web windows Windows platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants