feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
This commit is contained in:
109
lib/pages/apps/miniapp_shell_native.dart
Normal file
109
lib/pages/apps/miniapp_shell_native.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user