Skip to content
Back to archipelago.banuacoder.com

Feature SDKs (Pro)

webview_sdk_advanced Pro ​

Tier
Proversion 3.0.5
Command
archipelago generate webview_sdk_advanced
Generates
  • devtools/scripts/miniapp_signer
  • features/webview_advanced
Generates first
shared_consentwhen needed and missing, so there is nothing to install first
Depends on
shared_identifiers, flutter_l10ngenerate these first

Self-contained, standalone WebView Advanced feature SDK. Ships its own full WebView capability plus a bidirectional JS bridge (typed handlers + Promises), request/response interceptors for auth header injection, deeplink routing from WebView back to native, and a full mini-app container (manifest registry, bridge dispatcher, lifecycle state machine, capability handlers, and MiniAppContainerPage).

Independent of webview_sdk ​

webview_sdk_advanced does not require webview_sdk to be installed first, and does not extend or build on it — generate this brick alone if your app needs the JS bridge and/or mini-app hosting. Installing both is not a supported layering model; pick the one your project needs.

Variables ​

VariableTypeDefaultDescription
uiKitPackageNamestringapp_ui_kitName of the UI kit package used by the mini-app container. When generating into an existing workspace, pre_gen uses the name recorded in archipelago.yaml (detecting packages/*_ui_kit if it was never recorded) and fails with a clear message if that is ambiguous.
prefixstringAppUI kit class prefix, for example Acme for AcmeTheme. pre_gen reads it from archipelago.yaml (project.prefix), so you rarely set it

Mini-app hosting is not gated behind a variable — it is the point of this brick. If you don't need it, install webview_sdk (the basic, independent brick) instead.

analyticsInstalled is derived automatically (not a prompted variable) from whether infrastructure/analytics_api exists in the target project — when absent, the analytics bridge capability (handler, imports, tracker param) is stripped from the generated sources and the analytics_api dependency is omitted.

Usage ​

Interactive ​

bash
archipelago generate webview_sdk_advanced

Non-interactive (CI) ​

bash
archipelago generate webview_sdk_advanced --config my_config.json

Generated Package ​

This brick generates a single, complete, standalone package — it does not patch or inject files into any other package.

text
features/
└── webview_advanced/
    └── lib/src/
        ├── js_handler.dart                     # Abstract JsHandler<TArgs, TResult> base
        ├── js_handler_registry.dart            # JsHandlerRegistry (first-registration-wins)
        ├── webview_request.dart                # Immutable WebViewRequest value class
        ├── webview_request_interceptor.dart    # WebViewRequestInterceptor interface
        ├── webview_advanced_sdk.dart           # WebViewAdvancedSDK contract
        ├── webview_advanced_sdk_impl.dart      # flutter_inappwebview-backed impl
        ├── webview_controller.dart
        ├── webview_inappwebview_controller.dart
        ├── webview_inappwebview_controller_advanced.dart
        ├── di/
        ├── models/
        │   ├── js_message.dart
        │   ├── webview_advanced_config.dart    # WebViewAdvancedConfig extends WebViewConfig
        │   └── ...
        └── mini_app/
            ├── bridge/
            ├── bridge_dispatcher.dart
            ├── consent/                        # partner consent: planner, controller, sheet, settings pages
            ├── container/
            │   ├── mini_app_container.dart
            │   └── mini_app_container_page.dart
            ├── handlers/                       # analytics_handler.dart omitted when analytics_api is absent
            ├── interceptors/
            ├── lifecycle/
            ├── manifest/
            ├── manifest_v2/                    # signed manifests: verifier, rollback store, kill list, scopes
            ├── providers/
            ├── registry/
            ├── tokens/
            └── webview_bridge_binding.dart

Distinct type names ​

To avoid ambiguous-import collisions if both webview and webview_advanced ever end up in the same workspace (they are both kept in template_base for analyze/tests), the advanced package's FeatureSDK interface is WebViewAdvancedSDK / WebViewAdvancedSDKImpl — not WebViewSDK. Everything else (WebViewController, WebViewConfig, mini-app types, etc.) is duplicated wholesale rather than shared, per the independence design.

Bridge origin lockdown ​

Both bridges (window.archipelago.*'s jsHandlers, and the mini-app container's archipelago_bridge) are scoped to the main frame only (javaScriptHandlersForMainFrameOnly: true) and to WebViewAdvancedConfig .trustedOrigins (javaScriptHandlersOriginAllowList) whenever a trust policy is configured. Without this, an attacker-controlled <iframe> embedded in an otherwise-trusted page could invoke the bridge itself — the per-call trusted-domain check alone only ever sees the top-level frame's URL, not a different-origin subframe's. When no trust policy is configured (the default), neither restriction is applied, so unrelated internal JS handlers the plugin itself uses aren't broken for a caller who never set up a bridge at all.

Mini-App Container ​

The mini-app container — the headline Pro feature — is generated unconditionally for running config-driven web micro-apps inside your native shell.

How it works ​

  1. Manifest registry — MiniAppRegistry (ConfigMiniAppRegistry) loads mini-app definitions from assets/config/mini_apps.yaml via ArchipelagoConfigHelper, with an optional remote-config source layered on top (capability-guarded for net-new mini-app ids). The post-gen hook registers the asset in the host app's pubspec.yaml and writes a starter YAML file.
  2. Bridge dispatcher — MiniAppBridgeDispatcher validates (bridge version, per-invoke origin, capability grant) and routes incoming JS calls to registered BridgeCapabilityHandler implementations.
  3. Lifecycle state machine — MiniAppContainer tracks initializing → loading → ready → closing → closed (with an error branch) and exposes transitions as a MiniAppLifecycleEvent stream.
  4. Capability handlers — LifecycleHandler (always registered), NavigationHandler, ProfileHandler, StorageHandler, PermissionsHandler, AuthHandler, ShareHandler, HapticsHandler, and AnalyticsHandler (only generated when analytics_api is present in the target project).
  5. MiniAppContainerPage — A ready-to-use Flutter page that wires up the manifest, bridge dispatcher, and container widget.

Mini-app config file ​

The post-gen hook creates assets/config/mini_apps.yaml in your app:

yaml
mini_apps:
  - id: rewards
    entry_url: https://rewards.example.com/
    allowed_origins:
      - https://rewards.example.com
    bridge_origins:
      - https://rewards.example.com
    capabilities: [auth, profile, navigation, analytics]
    display:
      mode: fullscreen
      show_header: true
      title: "Rewards"

Delivery field ​

An entry can carry delivery: bundled | onDemand (default bundled) and a component name. onDemand needs a component; an entry without one, or with an unknown delivery, is skipped like any malformed entry.

yaml
  - id: rewards
    delivery: onDemand
    component: rewards

In template_base the host mounts an onDemand Flutter mini-app behind MiniAppDeliveryGate. The gate installs the component through the Pinisi contract (pinisi_api), shows progress and errors, offers Retry for retryable errors, and mounts only after the install finishes. With no Pinisi registered, an onDemand entry shows an error, not a crash. Bundled entries never touch Pinisi.

This brick ships the field and its validation only. MiniAppDeliveryGate and the pinisi_api dependency arrive with the Pinisi brick, so generated projects do not depend on a package no brick provides yet. Until then the field is parsed and ignored at mount time.

Token provider ​

Implement MiniAppTokenProvider to mint short-lived, scoped tokens for mini-app bridge sessions (see MiniAppScopedToken):

dart
class MyTokenProvider implements MiniAppTokenProvider {
  @override
  Future<MiniAppScopedToken> mintScopedToken(
    String miniAppId,
    List<String> scopes,
  ) async => MiniAppScopedToken(
    token: await tokenService.exchangeForMiniApp(miniAppId, scopes),
    expiresIn: 300,
    scopes: scopes,
    miniAppId: miniAppId,
  );
}

Manifest v2 (signed) ​

The YAML manifest above is trusted because it ships inside your app. Manifest v2 is for mini-apps whose manifest can come from anywhere (a bundled file, remote config, a server): it is trusted only because a registry key signed it.

A v2 manifest is a JWS (compact form). The header must say "alg": "EdDSA" (Ed25519). The package rejects every other algorithm, including none and HS256.

ManifestVerifier.verify returns a VerifiedManifest or a ManifestRejection. It runs these checks in order and stops at the first failure:

CheckRejection reason
The JWS, header and payload parsemalformed
alg is EdDSA and kid is a pinned keyunknownKey
The signature matches that keybadSignature
schema is 2invalid
exp is in the future (5 minutes of clock skew allowed)expired
version is not lower than the highest version already seen for this approlledBack
Invariants: ids, https origins, bridge_origins ⊆ allowed_origins ⊆ network_origins, known scopes, no token_strategy, lifetime of 90 days or less, min_host_bridge not newer than the hostinvalid
The kill list does not name the app, the publisher or the keykilled

Signing keys ​

The verifier trusts only the public keys in assets/config/mini_app_registry_keys.json:

json
{"keys": [
  {"kid": "2026-a", "x": "<base64url Ed25519 public key>"},
  {"kid": "2026-b", "x": "<base64url Ed25519 public key>"}
]}

Ship two keys: the current one and the next one. Then you can move to the next key without a store release. If the file is missing or has a bad entry, no key is pinned and every v2 manifest is rejected. Keep private keys off devices and out of the repository.

Rollback, kill list and scopes ​

  • ManifestRollbackStore saves the highest verified version of each app in MiniAppStorage. An older signed manifest is still rejected after the app restarts.
  • MiniAppKillList is read from the remote config key mini_app_kill_list. It is not signed, because it can only take access away: it removes apps (apps), publishers (publishers), signing keys (kids) or single scopes (scopes). It can never grant anything.
  • MiniAppScopeCatalogue lists the scopes a manifest may ask for, and the capability each one unlocks. An unknown scope rejects the manifest. VerifiedManifest.toRuntimeManifest(effectiveScopes) builds the v1 MiniAppManifest that the bridge dispatcher enforces.
  • The payment and consent capabilities come only from a verified v2 manifest. A YAML or remote-config v1 manifest cannot grant them.

PartnerManifestRegistry loads the bundled assets/config/mini_apps/*.jws files, then the remote config key mini_app_manifests_v2 (a JSON list of JWS strings). When two manifests have the same app, the higher version wins.

DI and startup ​

WebViewAdvancedGlobalModule registers PartnerManifestRegistry as a lazy singleton. Resolve it with getIt<PartnerManifestRegistry>(). The brick adds a degradable PartnerManifestInitializer to the host's post-launch initializers, which calls load() for you, so there is no manual step. load() never throws. Call refreshKillList() when remote config updates.

The registry reads these optional host registrations from the root GetIt:

RegistrationPurpose
MiniAppStorageKeeps anti-rollback versions. Without it, an in-memory store is used and rollback protection resets on restart.
RemoteManifestReader, instanceName: kMiniAppManifestsV2ReaderNameReturns the JSON list of JWS strings (mini_app_manifests_v2).
RemoteManifestReader, instanceName: kMiniAppKillListReaderNameReturns the kill list JSON (mini_app_kill_list).

Where to put signing keys ​

The brick writes assets/config/mini_app_registry_keys.json with {"keys": []} and declares it, and the empty assets/config/mini_apps/ folder, in your app pubspec.yaml. With zero keys the registry is fail closed: every v2 manifest is rejected as unknownKey. To trust a publisher, add its public keys to that file (format above), then put signed *.jws files in assets/config/mini_apps/ or serve them from remote config.

The brick also generates devtools/scripts/miniapp_signer/, a small Dart tool for this. Run it from the project root:

bash
# Make a key. The private key goes outside the repo.
dart run miniapp_signer keygen --kid 2026-a --out ~/.miniapp-keys/2026-a.jwk.json
# It prints {"kid":"2026-a","x":"..."}. Add that to "keys". Make and pin the
# next key (2026-b) now too, so you can rotate without a store release.

# Sign a manifest JSON file into a compact JWS.
dart run miniapp_signer sign --key ~/.miniapp-keys/2026-a.jwk.json --kid 2026-a \
  rewards.json -o apps/my_app/assets/config/mini_apps/rewards.jws

# Check a JWS against the pinned keys.
dart run miniapp_signer verify \
  --keys apps/my_app/assets/config/mini_app_registry_keys.json \
  apps/my_app/assets/config/mini_apps/rewards.jws
  • keygen will not write the private key inside a git work tree. --force overrides this and prints a warning. It never overwrites a key file, and the file gets mode 0600. Keep the key in a secret store (a CI secret, a KMS or a password manager), never in the repo.
  • sign refuses a manifest the app would reject at once: no schema: 2, or an exp that is in the past or more than 90 days after iat. The app checks the rest (origins, scopes, rollback, kill list).
  • verify runs the same envelope and signature checks as ManifestVerifier, plus schema and exp.
  • A golden test keeps the tool and the app in step: a JWS made by the signer is checked in features/webview_advanced/test/mini_app/manifest_v2/fixtures/ and must pass ManifestVerifier.

A partner mini-app gets a consent-class scope (identity, profile.*, location, camera, payment) only after the user allows it. The grants live in shared/consent_api (ScopedConsentGate, see shared_consent). This brick adds the UI and the rules on top. The brick generates shared_consent first when the project lacks it, and stops when consent_api is older than 0.1.1.

TypeRole
ConsentPlannerPure functions. forLaunch(manifest, decisions, syncAvailable:) and forRequest(manifest, decisions, scopes, syncAvailable:, declinedThisSession:) return a ConsentPlan (toAsk, missingRequired, blocked, allowsLaunch(answer)).
PartnerConsentControllercommit pushes through ScopedConsentSync first and stores only when the push succeeds. revoke and removeAccess update the device first, then push.
PartnerConsentSheetThe host-drawn sheet. PartnerConsentSheet.show(...) returns the answer, or null for "Not now" while a required scope is pending.
PartnerMiniAppsSettingsPageLists every partner with a grant. Opens PartnerMiniAppConsentDetailPage: one toggle per scope and "Remove access".

When the sheet appears:

  • First launch: every required scope, plus the optional ones, off by default.
  • Later launches: only required scopes that are not granted. A new manifest version that adds a required scope asks for that scope only. A required scope revoked in settings blocks launch and is asked again. A new optional scope is not asked at launch.
  • In session: forRequest asks only declared, optional, not-granted scopes. A scope declined in this session is not asked again.
  • No consent backend: with NoopScopedConsentSync, identity is never asked, so a mini-app that requires it stays blocked.
dart
final controller = PartnerConsentController(
  gate: getIt<ScopedConsentGate>(),
  sync: getIt<ScopedConsentSync>(),
  clearPartnerData: (subject) => partnerHost.clearData(subject),
);
final plan = ConsentPlanner.forLaunch(
  manifest,
  controller.gate.decisionsFor(manifest.subject),
  syncAvailable: controller.sync.isAvailable,
);
if (plan.toAsk.isNotEmpty) {
  final answer = await PartnerConsentSheet.show(
    context,
    manifest: manifest,
    toAsk: plan.toAsk,
    onAllow: (a) => controller.commit(
      manifest.subject, a, policyVersion: '${manifest.version}'),
  );
  if (!plan.allowsLaunch(answer)) return; // blocked
} else if (plan.blocked) {
  return; // a required scope cannot be granted on this host
}

The sheet shows the publisher logo, name and a "Partner" badge, then the required scopes without toggles, the optional scopes as toggles, and location and camera in their own group with a note that the system will also ask. If the push fails, the sheet stays open with an inline error so the user can retry, and nothing is stored. It cannot be dragged away during a save. Privacy and support links appear when you pass onOpenLink.

In the detail page, turning off a required scope first shows a warning ("Acme Rewards will not open until you allow this again"), then records the refusal and calls clearPartnerData. "Remove access" calls revokeSubject and clearPartnerData. If the push after a revoke fails, the refusal still holds on the device and a snackbar says it is not synced; it is not retried.

The pages take their dependencies in the constructor. The routes (/settings/mini-apps, /settings/mini-apps/:subject) and the clearPartnerData wiring come with the partner host. Strings are in l10n/en.i18n.json and l10n/id.i18n.json and follow the app locale (locale_core).

Web side of the bridge ​

The package ships archipelago-miniapp.js (ES module) and archipelago-miniapp.umd.js (script tag) in features/webview_advanced/web/. Copy one into your mini-app's web project. It gives the page invoke(capability, method, params), typed errors, host events and a mock mode for development in a normal browser. See the Mini-app bridge guide.

File upload, download, geolocation, and in-page camera/mic ​

WebViewAdvancedConfig gates four browser-level capabilities, all fail-closed (false/no-op by default):

FlagWiresDefault behavior when off
enableFileUploadonShowFileChooser (delegates to flutter_inappwebview's own default file-chooser UI)<input type="file"> has no effect
enableFileDownloadonDownloadStarting, forwarding each request on WebViewInAppWebViewAdvancedController.downloadStreamDownloads are silently ignored
enableWebRtconPermissionRequest — grants CAMERA/MICROPHONE/CAMERA_AND_MICROPHONEgetUserMedia() is denied
enableGeolocationonGeolocationPermissionsShowPromptnavigator.geolocation is denied

This package has no HTTP, file-system, or permission_handler dependency of its own — matching the bring-your-own pattern TokenExchangeHttpClient uses for token exchange:

  • Downloads: listen to downloadStream and fetch/save/share the file yourself (e.g. with dio + share_plus). A download is only forwarded while the current page is a trusted origin, and suggestedFilename is already sanitized to a bare basename (.., /, \, NUL stripped, falls back to 'download') — you must still apply your own size limit (don't blindly trust contentLength), a MIME allowlist, and never silently overwrite an existing file at the destination path.
  • enableWebRtc/enableGeolocation: also pass a runtimePermissionChecker — a Future<bool> Function(WebViewRuntimePermission permission) that reports whether the OS-level permission (camera, microphone, location) is already granted. Without one, both flags stay effectively denied even when true, since this package cannot request the OS permission itself.
dart
final config = WebViewAdvancedConfig(
  initialUrl: 'https://app.example.com',
  enableFileUpload: true,
  enableFileDownload: true,
  enableWebRtc: true,
  enableGeolocation: true,
  runtimePermissionChecker: (permission) async => switch (permission) {
    WebViewRuntimePermission.camera => Permission.camera.isGranted,
    WebViewRuntimePermission.microphone => Permission.microphone.isGranted,
    WebViewRuntimePermission.location => Permission.location.isGranted,
  },
);

final controller = webViewSDK.createController(config)
    as WebViewInAppWebViewAdvancedController;
controller.downloadStream.listen((request) async {
  // Fetch request.url yourself, then save/share the file.
});

Native permission keys ​

Only add these to your app's AndroidManifest.xml/Info.plist if you actually enable the matching flag above — requesting unused permissions is a common App Store/Play Store review rejection and a real privacy cost, so this brick does not inject them unconditionally into every generated app.

  • enableWebRtc — Android android.permission.CAMERA and android.permission.RECORD_AUDIO; iOS NSCameraUsageDescription and NSMicrophoneUsageDescription in Info.plist.
  • enableGeolocation — Android android.permission.ACCESS_FINE_LOCATION (and/or ACCESS_COARSE_LOCATION); iOS NSLocationWhenInUseUsageDescription in Info.plist.
  • enableFileUpload/enableFileDownload — no manifest/plist entry is needed for the default file-chooser/download flow on current Android and iOS versions.

Known limits ​

  • Top-level URL checks. Download gating (and the per-call check in jsHandlers) reads the top-level URL via getUrl(). The plugin's DownloadStartRequest does not say which frame started a download, so an iframe download on a trusted page passes, and a back/forward-cache navigation can briefly report a trusted URL. The bridge itself is not affected by iframes (main-frame-only). Treat downloadStream as untrusted.
  • No user-activation signal. Neither the bridge envelope nor the plugin reports user activation, so share and haptics cannot be gated on a real user gesture. share is rate-limited (one in flight, 2s cooldown, http/https only) instead; haptics is not throttled.
  • Filenames. suggestedFilename is sanitized (basename, control/bidi characters and .. stripped, 255-byte cap) but the host must not percent-decode it, and must enforce its own size/MIME limits.

Debug tooling: wiring the Bridge inspector ​

MiniAppContainer/MiniAppContainerPage accept an optional onBridgeEvent callback, forwarded to MiniAppBridgeDispatcher.onEvent. Wire app_debugger_miniapp's BridgeInspectorSDK.observerCallback into it to feed the debug panel's "Bridge" tab — no dependency on app_debugger_miniapp_api is added to webview_advanced itself.

onBridgeEvent must stay debug-only — always gate it behind kDebugMode (or an equivalent debug-only check) at the call site, as in the example below; never wire it unconditionally into a release build. auth capability results are redacted ('<redacted>') before reaching this callback regardless, since auth.getToken's result is a live, usable scoped token — but every other capability's params/result pass through unredacted:

dart
MiniAppContainerPage(
  manifest: manifest,
  webViewSDK: getIt<WebViewAdvancedSDK>(),
  onBridgeEvent: kDebugMode
      ? getIt<BridgeInspectorSDK>().observerCallback
      : null,
);

Theme capability ​

The theme capability lets a mini-app use the same branded theme as the host app, and follow it live. It needs no provider. Grant theme in the manifest. MiniAppContainer reads the ui_kit tokens from its own BuildContext (through context.colorsOrNull, radiusOrNull and spacingOrNull, so the generated class prefix does not matter).

Call or eventPayload
theme.get(){ brightness: 'light' | 'dark', tokens: { '--color-bg': '#EEF2FF', '--radius-md': '8px', '--spacing-lg': '1.0rem', ... } }
theme.changed (host event)same shape
  • tokens holds the semantic colors (background, foreground, action, border, status backgrounds, overlay), the radius scale and the spacing scale, for the current mode. The names are the ones the Tailwind preset uses (monorepo_toolkit ui-kit export-tailwind, see ui_kit_generator). Archipelago checks the names and the light and dark values against the exporter golden in its own template. Generated projects do not get that test, because a customized kit changes the values. Palettes, type, shadows and --color-overlay-light come only from the preset.
  • The container sends theme.changed 300 ms after the app theme settles. A system light/dark switch triggers it when the app follows the system (ThemeMode.system); an app pinned to one mode sends nothing, because its theme did not change (so the Material theme animation sends one event). It sends only while the page is on a bridge_origins entry. It also reports each push to onBridgeEvent, so the Bridge inspector lists it as theme.changed.
  • theme is read-only design tokens with no user data. It is in kRemoteSafeCapabilities, so a remote manifest can grant it.
  • In the page, await archipelago.followTheme() sets <html data-theme> and the CSS variables, and keeps them in sync. See the bridge guide.

Share and haptics capabilities ​

share and haptics follow the same host-supplied-provider pattern as storage/permissions/profile: pass null to leave the capability unregistered even if the manifest grants it.

dart
class AppShareProvider implements MiniAppShareProvider {
  @override
  Future<void> share({String? text, String? url}) =>
      SharePlus.instance.share(ShareParams(text: text, uri: url != null ? Uri.parse(url) : null));
}

class AppHapticsProvider implements MiniAppHapticsProvider {
  @override
  Future<void> impact(MiniAppHapticsImpact impact) => switch (impact) {
    MiniAppHapticsImpact.light => HapticFeedback.lightImpact(),
    MiniAppHapticsImpact.medium => HapticFeedback.mediumImpact(),
    MiniAppHapticsImpact.heavy => HapticFeedback.heavyImpact(),
    MiniAppHapticsImpact.selection => HapticFeedback.selectionClick(),
  };
}

MiniAppContainerPage(
  manifest: manifest,
  webViewSDK: getIt<WebViewAdvancedSDK>(),
  shareProvider: AppShareProvider(),
  hapticsProvider: AppHapticsProvider(),
);

Implement the other providers ​

Three more provider interfaces unlock the rest of the working capabilities this brick ships. All three follow the same shape: pass null to leave the capability unregistered even if the manifest grants it.

dart
class AppStorage implements MiniAppStorage {
  @override
  Future<String?> get(String namespacedKey) =>
      _prefs.getString(namespacedKey);

  @override
  Future<void> set(String namespacedKey, String value) =>
      _prefs.setString(namespacedKey, value);

  @override
  Future<void> remove(String namespacedKey) => _prefs.remove(namespacedKey);

  @override
  Future<void> clearNamespace(String namespacePrefix) async {
    for (final key in _prefs.getKeys()) {
      if (key.startsWith(namespacePrefix)) await _prefs.remove(key);
    }
  }
}

class AppPermissions implements MiniAppPermissions {
  @override
  Future<MiniAppPermissionStatus> request(MiniAppPermissionType type) async {
    final result = await _toPermission(type).request();
    return _toStatus(result);
  }

  @override
  Future<MiniAppPermissionStatus> status(MiniAppPermissionType type) async {
    final result = await _toPermission(type).status;
    return _toStatus(result);
  }
}

class AppProfileProvider implements MiniAppProfileProvider {
  @override
  Future<Map<String, dynamic>> getClaims() async => {
    'id': currentUser.id,
    'displayName': currentUser.name,
    'avatarUrl': currentUser.avatarUrl,
    'locale': currentUser.locale,
  };
}

MiniAppContainerPage(
  manifest: manifest,
  webViewSDK: getIt<WebViewAdvancedSDK>(),
  storage: AppStorage(),
  miniAppPermissions: AppPermissions(),
  profileProvider: AppProfileProvider(),
);

StorageHandler namespaces every key as miniapp:<manifestId>:<key> before calling MiniAppStorage — one mini-app cannot read another's data even if they share the same backing store. ProfileHandler intersects getClaims()'s result with MiniAppManifest.profileClaims — only whitelisted keys ever reach the mini-app.

Token Exchange 2.1.0+ ​

Each mini-app can independently opt into one of two auth.getToken strategies via token_strategy in its mini_apps.yaml entry:

yaml
mini_apps:
  - id: rewards
    # ...existing manifest fields...
    token_strategy: exchange        # default: interceptor (MVP)
    exchange_endpoint: /api/mini-apps/token-exchange   # optional override
  • token_strategy: interceptor (default) — unchanged MVP behavior: auth.getToken returns whatever your MiniAppAuthProvider.getToken supplies as-is, and MiniAppAuthInterceptor injects that same token on bridge-origin requests.
  • token_strategy: exchange — auth.getToken calls a backend token-exchange endpoint via ExchangeMiniAppTokenProvider (ships with this brick, implements MiniAppTokenProvider) to mint a short-lived, audience-bound token scoped to that mini-app. Wire your instance into MiniAppContainer.tokenProvider / MiniAppContainerPage.tokenProvider.

ExchangeMiniAppTokenProvider handles scope narrowing against the manifest's granted capabilities, in-memory caching until expires_in - 30s, single-flight de-duplication, expires_in clamping to 300s, and 401-refresh-retry-once session handling. You supply four seams: a TokenExchangeHttpClient (this package has no HTTP dependency of its own — bring your own client), a host session token supplier, a host session refresh hook, and a manifest lookup function.

The backend contract (request/response shape, error codes) is described in the token-exchange doc comments of the generated ExchangeMiniAppTokenProvider.

Integration ​

1. Implement Handlers ​

dart
class GetTokenHandler implements JsHandler<void, String> {
  @override
  String get handlerName => 'getToken';

  @override
  void parseArgs(List<dynamic> args) {}

  @override
  Future<String> handle(void args) async =>
      await tokenService.getAccessToken();

  @override
  Map<String, dynamic> serializeResult(String result) => {'token': result};
}

2. Create Config ​

dart
final config = WebViewAdvancedConfig(
  initialUrl: 'https://app.example.com',
  trustedOrigins: WebViewTrustPolicy.origins(['https://app.example.com']),
  interceptors: [AuthInterceptor(tokenProvider)],
  jsHandlers: [GetTokenHandler()],
);

3. Create Controller ​

dart
final controller = getIt<WebViewAdvancedSDK>().createController(config);

4. Call from JavaScript ​

js
const token = await window.archipelago.getToken();

5. Post Native Messages to JS ​

dart
await controller.postMessageToJs('userLoggedIn', {'userId': '123'});
js
window.archipelago.onNativeMessage = function(name, payload) {
  if (name === 'userLoggedIn') console.log('User:', payload.userId);
};

Auth Interceptor Example ​

dart
class AuthInterceptor implements WebViewRequestInterceptor {
  const AuthInterceptor(this._tokenProvider);
  final TokenProvider _tokenProvider;

  @override
  Future<WebViewRequest?> intercept(WebViewRequest request) async {
    final token = await _tokenProvider.getAccessToken();
    return request.copyWith(
      headers: {...request.headers, 'Authorization': 'Bearer $token'},
    );
  }
}

Android — R8/ProGuard ​

The post-gen hook appends flutter_inappwebview's callback keep rules plus a @android.webkit.JavascriptInterface keep (for the JS bridge) to the host app's proguard-rules.pro automatically — no manual setup needed. This only applies when the host app was generated by flutter_modular_monorepo or app_scaffold (both ship the archipelago:proguard:keep-rules marker pair this brick writes into); if the marker pair is missing the contribution is skipped silently.

Provider Capability ​

Featureflutter_inappwebview
JS bridgeFull (Promise-based, per-handler channels)
Header injectionMain navigation load only (via loadUrl; sub-resources are not intercepted)
File uploadFull (onShowFileChooser, requires enableFileUpload)
File downloadFull (onDownloadStarting -> downloadStream, requires enableFileDownload)

Changelog ​

Run archipelago brick info webview_sdk_advanced to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.