Setting Up the WebView SDK Advanced
ProWhat you'll learn
- Generating the standalone
webview_sdk_advancedbrick (it does not requirewebview_sdkfirst) - 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) andexchangetoken strategies for mini-app auth
Prerequisites
- An existing Archipelago monorepo (see Monorepo Scaffolding)
- An active Pro or Enterprise subscription
Not required:
webview_sdkdoes not need to be generated first.webview_sdk_advancedis 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, generatewebview_sdk(Free) instead — installing both in the same app is not a supported layering model.
Step 1: Generate the WebView SDK Advanced
archipelago generate webview_sdk_advancedYou 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:
{
"uiKitPackageName": "myapp_ui_kit"
}archipelago generate webview_sdk_advanced --config webview_advanced_config.jsonThe 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.dartEverything 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>:
// 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:
// 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
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:
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
// 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:
await controller.postMessageToJs('userLoggedIn', {'userId': '123'});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 registry —
ConfigMiniAppRegistryloads mini-app definitions fromassets/config/mini_apps.yamlat startup viaArchipelagoConfigHelper. - Bridge dispatcher —
MiniAppBridgeDispatchervalidates (bridge version, per-invoke origin, capability grant) and routes JS calls to registeredBridgeCapabilityHandlers. - Capability handlers —
LifecycleHandler(always registered),NavigationHandler,ProfileHandler,StorageHandler,PermissionsHandler,AuthHandler, andAnalyticsHandler(only generated whenanalytics_apiis 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:
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:
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.getTokenreturns whateverMiniAppAuthProvider.getTokensupplies as-is, andMiniAppAuthInterceptorinjects that same token on bridge-origin requests. No backend call is made.exchange—auth.getTokencalls a backend token-exchange endpoint viaExchangeMiniAppTokenProvider(ships with this brick) to mint a short-lived, audience-bound token scoped to that mini-app. Wire your instance intoMiniAppContainer.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:
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
| Customization | Where to Change |
|---|---|
| Add a new JS handler | Create a class implementing JsHandler, add to jsHandlers list |
| Block certain requests | Return null from an interceptor |
| Restrict bridge to one origin | Add it to WebViewTrustPolicy.origins([...]) on trustedOrigins |
| Multiple interceptors | Pass multiple items in the interceptors list — they run in order |
| Add a mini-app | Add an entry under mini_apps: in assets/config/mini_apps.yaml |
| Switch a mini-app to backend token exchange | Set token_strategy: exchange on its manifest entry and wire an ExchangeMiniAppTokenProvider |
Next Steps
- Set up the Auth SDK to supply tokens to
GetTokenHandler,AuthInterceptor, andMiniAppAuthProvider - Configure monitoring to track JS bridge errors and interceptor failures
- Set up the UI Kit to style loading states shown while the WebView initializes