Skip to main content

Embedding the Visitor WebView in an App

When you embed the visitor chat page inside your app's WebView, use AppBridge to enable two-way communication between the H5 page and the native layer.

Visitor page URL format: ${host}/direct/${appid}?source=webview

The source=webview parameter hides the back button in the upper-left corner of the visitor page.

Technical Architecture

  • Framework: Flutter + webview_flutter 4.13.0+
  • Messaging mechanism: JavaScript Channel (channel name: AppBridgeChannel)
  • H5 call pattern: window.AppBridge.call(method, params, callback)
  • App-side handling: Route messages through a unified BridgeAction class

Required: Inject the AppBridge compatibility layer

After each page load completes (onPageFinished), you must inject the compatibility script. Otherwise window.AppBridge will not be available.

WebView initialization (required settings)

import 'package:business_module/business_module.dart';

void initWebViewController() async {
// ... platform-specific setup omitted, see the full example

webViewController
..addJavaScriptChannel(
'AppBridgeChannel',
onMessageReceived: (message) {
BridgeAction().handleJSMessage(
BridgeMessageBean.fromJsonString(message.message),
controller: webViewController,
);
},
)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) async {
await _injectAppBridgeCompatibilityLayer();
},
));
}

Compatibility layer injection script

Future<void> _injectAppBridgeCompatibilityLayer() async {
const script = r'''
(function() {
window.AppBridgeChannel_native = {
postMessage: function(msg) {
if (window.AppBridgeChannel?.postMessage) {
window.AppBridgeChannel.postMessage(msg);
}
}
};

window.AppBridge = window.AppBridge || {};
window.AppBridge._callbacks = window.AppBridge._callbacks || {};

window.AppBridge.call = function(method, data, callback) {
return new Promise(function(resolve, reject) {
var callbackId = null;
if (typeof callback === 'function') {
callbackId = 'cb_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
window.AppBridge._callbacks[callbackId] = callback;
}
var msg = { method: method, params: data || {}, callback: callbackId };
window.AppBridgeChannel_native.postMessage(JSON.stringify(msg));
resolve({ status: 'sent', callbackId: callbackId });
});
};

window.AppBridge.invokeCallback = function(callbackId, result) {
var cb = window.AppBridge._callbacks[callbackId];
if (cb) { cb(result); delete window.AppBridge._callbacks[callbackId]; }
};
})();
''';
await webViewController.runJavaScript(script);
}

Data Models

class BridgeMessageBean {
final String method;
final Map<String, dynamic>? params;
final String? callback;

factory BridgeMessageBean.fromJsonString(String jsonString) {
final json = jsonDecode(jsonString);
return BridgeMessageBean(
method: json['method'] ?? '',
params: json['params'] != null ? Map<String, dynamic>.from(json['params']) : null,
callback: json['callback'],
);
}
}

class BridgeCallbackBean {
final int code;
final String msg;
final Map<String, dynamic>? data;

String toString() => jsonEncode({'code': code, 'msg': msg, 'data': data});
}

Supported Bridge Methods

download - Download a file

window.AppBridge.call('download', { url: 'https://...', type: 'image' }, (res) => {
console.log(res.code === 1 ? 'Download successful' : res.msg);
});
ParameterTypeDescription
urlstringFile URL
typestringimage / video / other

new_message - Play the new message sound

window.AppBridge.call('new_message', {});

android_recording_permission - Request microphone permission

window.AppBridge.call('android_recording_permission', {}, (res) => {
if (res?.data?.permission) {
console.log('Recording permission granted');
}
});

Callback payload: { code: 1, msg: 'success', data: { permission: true/false } }

on_ring / off_ring - Ring control

window.AppBridge.call('on_ring', {}); // Enable ringtone
window.AppBridge.call('off_ring', {}); // Disable ringtone

App-Side Message Handling

class BridgeAction {
late BridgeMessageBean message;
late WebViewController webViewController;

Future<void> handleJSMessage(BridgeMessageBean message, {required WebViewController controller}) async {
this.message = message;
webViewController = controller;

switch (message.method) {
case 'download': await _handleDownload(message.params); break;
case 'new_message': await AudioPlayerUtil().togglePlay(R.files.alert); break;
case 'android_recording_permission': await _handleRecordingPermission(); break;
case 'on_ring': await _handleRingControl(true); break;
case 'off_ring': await _handleRingControl(false); break;
}
}

Future<void> callbackJS(BridgeCallbackBean data) async {
final callbackId = message.callback ?? '';
if (callbackId.isEmpty) return;
await webViewController.runJavaScript(
"window.AppBridge?.invokeCallback('$callbackId', ${data.toString()})"
);
}
}

Permissions

Before using recording or camera features, declare the required permissions on the native side:

  • Android: Add RECORD_AUDIO and CAMERA permissions in AndroidManifest.xml
  • iOS: Add microphone and camera usage descriptions in Info.plist

Debugging

Enable WebView debugging during development:

AndroidWebViewController.enableDebugging(true);

To confirm injection succeeded, the console should output [AppBridge] injection complete if you keep the console.log.