110 lines
3.3 KiB
Dart
110 lines
3.3 KiB
Dart
// SPDX-FileCopyrightText: 2026 TatuChat
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
import 'package:tatchat/l10n/l10n.dart';
|
|
|
|
/// Native implementation of [MiniAppShell] using an isolated [InAppWebView].
|
|
///
|
|
/// The mini app HTML is shipped with a small bridge shim injected before
|
|
/// `</body>` that forwards `window.parent.postMessage` calls to Dart through
|
|
/// `flutter_inappwebview`'s JavaScript handler, and delivers the response
|
|
/// back to the mini app via a `postMessage` with the same `requestId`.
|
|
class MiniAppShell extends StatefulWidget {
|
|
final String html;
|
|
final String appId;
|
|
final String title;
|
|
final Future<Map<String, Object?>> Function(Map<String, dynamic> request)
|
|
onRequest;
|
|
|
|
const MiniAppShell({
|
|
super.key,
|
|
required this.html,
|
|
required this.appId,
|
|
required this.title,
|
|
required this.onRequest,
|
|
});
|
|
|
|
@override
|
|
State<MiniAppShell> createState() => _MiniAppShellState();
|
|
}
|
|
|
|
class _MiniAppShellState extends State<MiniAppShell> {
|
|
static const String _handlerName = 'com.tatuchat.miniapp.bridge';
|
|
|
|
InAppWebViewController? _webViewController;
|
|
|
|
String get _injectedHtml {
|
|
const shim =
|
|
'''
|
|
<script>
|
|
window.addEventListener('message', function (event) {
|
|
var msg = event.data;
|
|
if (!msg || msg.api !== 'fromMiniApp') return;
|
|
window.flutter_inappwebview.callHandler('$_handlerName', msg).then(function (response) {
|
|
var reply = Object.assign({}, msg, { response: response });
|
|
window.postMessage(reply, '*');
|
|
});
|
|
});
|
|
</script>
|
|
''';
|
|
final bodyEnd = widget.html.lastIndexOf('</body>');
|
|
if (bodyEnd == -1) {
|
|
return '${widget.html}\n$shim';
|
|
}
|
|
return widget.html.replaceRange(bodyEnd, bodyEnd, '\n$shim');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.title),
|
|
backgroundColor: theme.colorScheme.primary,
|
|
foregroundColor: theme.colorScheme.onPrimary,
|
|
actions: [
|
|
IconButton(
|
|
tooltip: L10n.of(context).refresh,
|
|
icon: const Icon(Icons.refresh_rounded),
|
|
onPressed: () => _webViewController?.reload(),
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: theme.colorScheme.surface,
|
|
body: InAppWebView(
|
|
initialSettings: InAppWebViewSettings(
|
|
javaScriptEnabled: true,
|
|
javaScriptCanOpenWindowsAutomatically: false,
|
|
domStorageEnabled: true,
|
|
transparentBackground: false,
|
|
verticalScrollBarEnabled: true,
|
|
horizontalScrollBarEnabled: false,
|
|
),
|
|
initialData: InAppWebViewInitialData(
|
|
data: _injectedHtml,
|
|
mimeType: 'text/html',
|
|
encoding: 'utf8',
|
|
baseUrl: WebUri('https://tatuchat.local'),
|
|
),
|
|
onWebViewCreated: (controller) {
|
|
_webViewController = controller;
|
|
controller.addJavaScriptHandler(
|
|
handlerName: _handlerName,
|
|
callback: (args) async {
|
|
if (args.isEmpty) return null;
|
|
final raw = args.first;
|
|
if (raw is Map<String, dynamic>) {
|
|
return widget.onRequest(Map<String, dynamic>.from(raw));
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|