Files
tatuchat/lib/pages/apps/miniapp_shell_web.dart
Welton Moura 6c689d1f34
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: tela de apps com logos/menu de exclusao, correcao de tema nas mini apps e sidebar de navegacao
2026-09-19 00:08:35 -03:00

151 lines
4.1 KiB
Dart

// SPDX-FileCopyrightText: 2026 TatuChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:js_interop';
import 'dart:ui_web' as ui_web;
import 'package:material_ui/material_ui.dart';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/utils/miniapp_bridge.dart';
import 'package:web/web.dart' as web;
/// Web implementation of [MiniAppShell] rendering the mini app into a sandboxed
/// iframe.
///
/// The mini app talks to TatuChat through `postMessage`. This widget listens on
/// `window` for `fromMiniApp` requests, forwards them to the Dart bridge and
/// posts the response back to the iframe's `contentWindow`.
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 int _viewTypeCounter = 0;
late final String _viewType;
web.HTMLIFrameElement? _iframe;
web.HTMLDivElement? _frameElement;
JSFunction? _messageListener;
@override
void initState() {
super.initState();
_viewType = 'tatchat-miniapp-${_viewTypeCounter++}';
ui_web.platformViewRegistry.registerViewFactory(
_viewType,
(int viewId) => _createElement(),
);
final messageListener = _onMessage.toJS;
_messageListener = messageListener;
web.window.addEventListener('message', messageListener);
}
@override
void dispose() {
final listener = _messageListener;
if (listener != null) {
web.window.removeEventListener('message', listener);
}
super.dispose();
}
web.HTMLIFrameElement _buildIframe() {
final iframe =
web.document.createElement('iframe') as web.HTMLIFrameElement;
iframe.style
..width = '100%'
..height = '100%'
..border = 'none';
iframe.setAttribute(
'sandbox',
'allow-scripts allow-forms allow-same-origin',
);
iframe.setAttribute('srcdoc', widget.html);
return iframe;
}
web.HTMLElement _createElement() {
final div = web.document.createElement('div') as web.HTMLDivElement;
div.style
..width = '100%'
..height = '100%'
..margin = '0'
..padding = '0';
final iframe = _buildIframe();
div.append(iframe);
_iframe = iframe;
_frameElement = div;
return div;
}
void _reload() {
final div = _frameElement;
if (div == null) return;
_iframe?.remove();
final iframe = _buildIframe();
div.append(iframe);
_iframe = iframe;
}
void _onMessage(web.Event event) {
if (!event.isA<web.MessageEvent>()) return;
final messageEvent = event as web.MessageEvent;
final data = messageEvent.data?.dartify();
if (data is! Map) return;
final request = Map<String, dynamic>.from(data);
if (request['api'] != MiniappBridge.api) return;
final iframeWindow = _iframe?.contentWindow;
if (iframeWindow == null) return;
final requestId = request['requestId'];
final widgetId = request['widgetId'];
widget.onRequest(request).then((result) {
iframeWindow.postMessage(
{
'api': MiniappBridge.api,
'widgetId': widgetId,
'requestId': requestId,
'response': result,
}.jsify(),
'*'.toJS,
);
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: theme.colorScheme.surface,
surfaceTintColor: Colors.transparent,
foregroundColor: theme.colorScheme.onSurface,
actions: [
IconButton(
tooltip: L10n.of(context).refresh,
icon: const Icon(Icons.refresh_rounded),
onPressed: _reload,
),
],
),
backgroundColor: theme.colorScheme.surface,
body: HtmlElementView(viewType: _viewType),
);
}
}