Skip to content

Setting Up the WebView SDK Advanced

Pro

What you'll learn

  • Generating the standalone webview_sdk_advanced brick (it does not require webview_sdk first)
  • Implementing typed JS handlers behind a bidirectional, Promise-based bridge
  • Injecting headers into the main navigation load with WebViewRequestInterceptor
  • Hosting config-driven mini-apps with the built-in mini-app container
  • Choosing between the interceptor (MVP) and exchange token strategies for mini-app auth

Prerequisites

Not required: webview_sdk does not need to be generated first. webview_sdk_advanced is a fully standalone, self-contained package — it does not extend or patch the basic brick. If you only need plain URL loading, navigation, and one-way JS calls, generate webview_sdk (Free) instead — installing both in the same app is not a supported layering model.

Step 1: Generate the WebView SDK Advanced

bash
archipelago generate webview_sdk_advanced

You will be prompted for:

  • uiKitPackageName — your UI kit package name, used by the mini-app container (default: app_ui_kit)

Or use a config file:

json
{
  "uiKitPackageName": "myapp_ui_kit"
}
bash
archipelago generate webview_sdk_advanced --config webview_advanced_config.json

The bidirectional JS bridge, request interceptors, and the mini-app container are not gated behind variables — they are always generated. There's nothing to toggle; this is the point of the brick.

Step 2: Understand the Generated Structure

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

features/
└── webview_advanced/
    └── lib/src/
        ├── js_handler.dart                      # Abstract JsHandler<TArgs, TResult>
        ├── 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_inappwebview_controller_advanced.dart  # Bridge + interceptor controller
        ├── di/
        │   └── webview_advanced_global_module.dart
        ├── models/
        │   ├── js_message.dart
        │   ├── webview_advanced_config.dart      # WebViewAdvancedConfig extends WebViewConfig
        │   └── ...
        └── mini_app/
            ├── bridge_dispatcher.dart             # MiniAppBridgeDispatcher
            ├── bridge/
            ├── container/
            │   ├── mini_app_container.dart
            │   └── mini_app_container_page.dart
            ├── handlers/                          # analytics_handler.dart omitted when analytics_api is absent
            ├── interceptors/
            ├── manifest/
            │   └── mini_app_manifest.dart          # MiniAppManifest, MiniAppTokenStrategy
            ├── registry/
            ├── tokens/
            │   └── exchange_mini_app_token_provider.dart
            └── webview_bridge_binding.dart

Everything is imported from one barrel: package:webview_advanced/webview_advanced.dart.

The generator wires WebViewAdvancedGlobalModule into your shell app's @InjectableInit(externalPackageModulesBefore: [...]), adds the webview_advanced path dependency to your app's pubspec.yaml, declares assets/config/mini_apps.yaml as a Flutter asset, and seeds that file with a starter config — all automatically. There's no manual DI step.

Step 3: Implement a JS Handler

Each native capability you expose to the web page is a typed class implementing JsHandler<TArgs, TResult>:

dart
// lib/src/js_handlers/get_token_handler.dart
import 'package:webview_advanced/webview_advanced.dart';

class GetTokenHandler implements JsHandler<void, String> {
  const GetTokenHandler(this._tokenService);

  final TokenService _tokenService;

  @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};
}

handlerName becomes the method name exposed on window.archipelago — the SDK injects a JS shim per handler that wraps the call in a Promise.

Step 4: Implement a Request Interceptor

Interceptors run in list order against every navigation request — the most common use case is injecting an Authorization header:

dart
// lib/src/interceptors/auth_interceptor.dart
import 'package:webview_advanced/webview_advanced.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',
      },
    );
  }
}

Return null to abort the request; return the (possibly modified) request to let it through. Header/method mutation is applied to the main navigation load only, via controller.loadUrl(...) — this covers the initial load and any subsequent loadUrl call. Sub-resource requests (images, XHR, fonts, etc.) only support the abort behaviour via shouldInterceptRequest; they cannot have headers injected without a synthetic-response round trip, which this package does not implement.

Step 5: Build the Config and Controller

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

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

trustedOrigins restricts which origins the JS bridge responds to — an empty/default policy trusts nothing, so always populate this in production. Load a placeholder first, then navigate via controller.loadUrl(...) so the interceptor chain actually runs — setting initialUrlRequest directly to config.initialUrl would bypass it entirely:

dart
InAppWebView(
  initialUrlRequest: URLRequest(url: WebUri('about:blank')),
  onWebViewCreated: (c) {
    controller.attachNativeController(c);
    controller.loadUrl(config.initialUrl);
  },
  shouldOverrideUrlLoading: controller.shouldOverrideUrlLoading,
  shouldInterceptRequest: controller.shouldInterceptRequest,
  onProgressChanged: controller.onProgressChanged,
  onLoadStart: controller.onLoadStart,
  onLoadStop: controller.onLoadStop,
  onReceivedError: controller.onReceivedError,
);

Step 6: Call Native Handlers from JavaScript

js
// In your web app
async function loadUserData() {
  const result = await window.archipelago.getToken();
  console.log('Access token:', result.token);
}

The method name (getToken) must match the handler's handlerName exactly. Post events from Dart back to the page without waiting for a JS call:

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

Step 7: The Mini-App Container

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

  • Manifest registryConfigMiniAppRegistry loads mini-app definitions from assets/config/mini_apps.yaml at startup via ArchipelagoConfigHelper.
  • Bridge dispatcherMiniAppBridgeDispatcher validates (bridge version, per-invoke origin, capability grant) and routes JS calls to registered BridgeCapabilityHandlers.
  • Capability handlersLifecycleHandler (always registered), NavigationHandler, ProfileHandler, StorageHandler, PermissionsHandler, AuthHandler, and AnalyticsHandler (only generated when analytics_api is present in your project).
  • MiniAppContainerPage — a ready-to-use page that wires up the manifest, bridge dispatcher, and container widget.

The post-gen hook already created assets/config/mini_apps.yaml for you, seeded with field documentation and a commented example:

yaml
mini_apps: []
# Example:
# mini_apps:
#   - id: rewards
#     entry_url: https://rewards.example.com/
#     allowed_origins:
#       - https://rewards.example.com
#       - https://cdn.example.com
#     bridge_origins:
#       - https://rewards.example.com
#     capabilities: [auth, profile, navigation, analytics]
#     bridge_version: "1.0"
#     # token_strategy: exchange        # default: interceptor (MVP)
#     # exchange_endpoint: /api/mini-apps/token-exchange   # optional override
#     display:
#       mode: fullscreen
#       show_header: true
#       title: "Rewards"

Uncomment and fill in an entry (or register one in code via ConfigMiniAppRegistry.register(MiniAppManifest(...))), then launch it:

dart
Navigator.of(context).push(
  MaterialPageRoute(
    fullscreenDialog: manifest.display.mode == MiniAppDisplayMode.fullscreen,
    builder: (_) => MiniAppContainerPage(
      manifest: manifest,
      webViewSDK: getIt<WebViewAdvancedSDK>(),
      tracker: getIt<AnalyticTracker>(), // omitted if analytics_api is absent
      navigator: getIt<ExternalNavigator>(),
      allowedDeepLinks: const ['/rewards/', '/profile/'],
    ),
  ),
);

Step 8: Choose a Token Strategy

Each mini-app independently opts into one of two auth.getToken strategies via token_strategy in its manifest entry:

  • interceptor (default, MVP) — auth.getToken returns whatever MiniAppAuthProvider.getToken supplies as-is, and MiniAppAuthInterceptor injects that same token on bridge-origin requests. No backend call is made.
  • exchangeauth.getToken calls a backend token-exchange endpoint via ExchangeMiniAppTokenProvider (ships with this brick) to mint a short-lived, audience-bound token scoped to that mini-app. Wire your instance into MiniAppContainer.tokenProvider / MiniAppContainerPage.tokenProvider. See the Archipelago-ac3d spec (docs/plans/2026-07-10-miniapp-token-exchange-spec.md) for the full backend contract.

Calling auth.getToken from the mini-app's JS

Mini-app bridge calls go through a single native handler, archipelago_bridge, using the wire envelope documented on kMiniAppBridgeVersion:

js
const response = await window.flutter_inappwebview.callHandler(
  'archipelago_bridge',
  JSON.stringify({
    v: '1.0',
    id: crypto.randomUUID(),
    capability: 'auth',
    method: 'getToken',
    params: {},
  }),
);

if (response.ok) {
  console.log('token:', response.result.token);
} else {
  console.error(response.error.code, response.error.message);
}

This is a different, lower-level channel than the per-handler window.archipelago.<handlerName> bridge from Step 6 — it's the one used internally by the mini-app container, not something you wire up yourself.

Common Customizations

CustomizationWhere to Change
Add a new JS handlerCreate a class implementing JsHandler, add to jsHandlers list
Block certain requestsReturn null from an interceptor
Restrict bridge to one originAdd it to WebViewTrustPolicy.origins([...]) on trustedOrigins
Multiple interceptorsPass multiple items in the interceptors list — they run in order
Add a mini-appAdd an entry under mini_apps: in assets/config/mini_apps.yaml
Switch a mini-app to backend token exchangeSet token_strategy: exchange on its manifest entry and wire an ExchangeMiniAppTokenProvider

Next Steps

Built by Banua Coder