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=webviewparameter hides the back button in the upper-left corner of the visitor page.
Technical Architecture
- Framework: Flutter +
webview_flutter4.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
BridgeActionclass
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);
});
| Parameter | Type | Description |
|---|---|---|
url | string | File URL |
type | string | image / 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_AUDIOandCAMERApermissions inAndroidManifest.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.