Files
tatuchat/lib/pages/apps/miniapp_shell_native.dart
Welton Moura 1f692b1e4e
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
2026-09-18 15:11:12 -03:00

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;
},
);
},
),
);
}
}