feat: mini apps customizados com importacao por arquivo/URL, paleta de cores, avatar autenticado e documentacao
This commit is contained in:
149
lib/pages/apps/miniapp_shell_web.dart
Normal file
149
lib/pages/apps/miniapp_shell_web.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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:flutter/material.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.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).refresh,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
onPressed: _reload,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
body: HtmlElementView(viewType: _viewType),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user