// SPDX-FileCopyrightText: 2026 TatuChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'package:flutter/services.dart'; import 'package:material_ui/material_ui.dart'; import 'package:matrix/matrix.dart'; import 'package:tatchat/config/miniapp_config.dart'; import 'package:tatchat/utils/miniapp_bridge.dart'; import 'package:tatchat/widgets/matrix.dart'; import 'miniapp_shell_stub.dart' if (dart.library.js_interop) 'miniapp_shell_web.dart' if (dart.library.ffi) 'miniapp_shell_native.dart'; /// Generic page that renders a mini app. The mini app is a self-contained HTML /// document that talks to the TatuChat client through the [MiniappBridge] /// protocol (see `assets/miniapps/`). class MiniAppPage extends StatefulWidget { final String appId; const MiniAppPage({super.key, required this.appId}); @override State createState() => _MiniAppPageState(); } class _MiniAppPageState extends State { Client get client => Matrix.of(context).client; MiniAppConfig? _config; String? _html; bool _notFound = false; @override void initState() { super.initState(); _load(); } Future _load() async { final config = resolveMiniAppConfig(client, widget.appId); if (config == null) { if (!mounted) return; setState(() => _notFound = true); return; } if (config.htmlAsset != null) { final html = await rootBundle.loadString(config.htmlAsset!); if (!mounted) return; setState(() { _config = config; _html = html; }); return; } // Imported app: the HTML source lives in the account data store. if (!mounted) return; setState(() { _config = config; _html = config.html; }); } /// Entry point of the bridge: called by the web view/iframe with a request /// coming from the mini app's JavaScript. Future> handleRequest(Map request) { final scheme = Theme.of(context).colorScheme; final brightness = Theme.of(context).brightness; return MiniappBridge.handleRequest( client, widget.appId, request, palette: MiniappBridge.buildPalette(scheme, brightness), ); } @override Widget build(BuildContext context) { final themeColorScheme = Theme.of(context).colorScheme; if (_notFound) { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), title: Text(widget.appId), backgroundColor: themeColorScheme.surface, surfaceTintColor: Colors.transparent, foregroundColor: themeColorScheme.onSurface, ), body: const Center(child: Text('App not found')), ); } final html = _html; final config = _config; if (html == null || config == null) { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), title: Text(_config?.name ?? _config?.id ?? widget.appId), backgroundColor: themeColorScheme.surface, surfaceTintColor: Colors.transparent, foregroundColor: themeColorScheme.onSurface, ), body: const Center( child: CircularProgressIndicator.adaptive(strokeWidth: 2), ), ); } return MiniAppShell( key: ValueKey(widget.appId), html: html, appId: widget.appId, title: config.name ?? config.id, onRequest: handleRequest, ); } }