feat(webapp): мини-приложения через webAppInitData, вход в Сферум

This commit is contained in:
klockky
2026-06-07 18:28:59 +00:00
parent 9bc3565922
commit 4a0eeb6ea2
6 changed files with 292 additions and 1 deletions
+45
View File
@@ -0,0 +1,45 @@
import '../api.dart';
import '../../core/protocol/opcode_map.dart';
class WebAppLaunch {
final String url;
const WebAppLaunch({required this.url});
}
class WebAppModule {
static const int sferumBotId = 2340319;
final Api _api;
WebAppModule(this._api);
Future<WebAppLaunch> fetchLaunch(int botId) async {
if (_api.state != SessionState.online) {
throw const WebAppUnavailable('Нет соединения с сервером');
}
final packet = await _api.sendRequest(Opcode.webAppInitData, {
'botId': botId,
});
if (!packet.isOk) {
throw const WebAppUnavailable('Не удалось открыть мини-приложение');
}
final data = packet.payload;
final url = (data is Map) ? data['url'] as String? : null;
if (url == null || url.isEmpty) {
throw const WebAppUnavailable('Сервер не вернул адрес приложения');
}
return WebAppLaunch(url: url);
}
Future<WebAppLaunch> fetchSferum() => fetchLaunch(sferumBotId);
}
class WebAppUnavailable implements Exception {
final String message;
const WebAppUnavailable(this.message);
@override
String toString() => message;
}
+13 -1
View File
@@ -15,6 +15,7 @@ import '../../widgets/komet_avatar.dart';
import '../../widgets/sheet_helpers.dart';
import '../auth/login_screen.dart';
import '../auth/proxy_settings_sheet.dart';
import '../webapp/web_app_screen.dart';
import 'cloud_storage_screen.dart';
import 'customization_screen.dart';
import 'performance_screen.dart';
@@ -252,9 +253,20 @@ class _SettingsTabState extends State<SettingsTab> {
icon: Symbols.badge,
label: 'Цифровой ID',
),
const _SettingsItem(
_SettingsItem(
icon: Symbols.language,
label: 'Войти в Сферум',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebAppScreen(
title: 'Сферум',
loader: () => webAppModule.fetchSferum(),
),
),
);
},
),
_SettingsItem(
icon: Symbols.info,
@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/webapp.dart';
class WebAppScreen extends StatefulWidget {
final String title;
final Future<WebAppLaunch> Function() loader;
const WebAppScreen({
super.key,
required this.title,
required this.loader,
});
@override
State<WebAppScreen> createState() => _WebAppScreenState();
}
class _WebAppScreenState extends State<WebAppScreen> {
InAppWebViewController? _controller;
WebAppLaunch? _launch;
String? _loadError;
double _progress = 0;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
_loadError = null;
_launch = null;
});
try {
final launch = await widget.loader();
if (!mounted) return;
setState(() => _launch = launch);
} catch (e) {
if (!mounted) return;
setState(() => _loadError = e.toString());
}
}
Future<bool> _handleBack() async {
final controller = _controller;
if (controller != null && await controller.canGoBack()) {
await controller.goBack();
return false;
}
return true;
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final navigator = Navigator.of(context);
if (await _handleBack()) navigator.pop();
},
child: Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
title: Text(widget.title),
leading: IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.of(context).maybePop(),
),
actions: [
IconButton(
icon: const Icon(Symbols.refresh),
onPressed: _launch == null
? null
: () => _controller?.reload(),
),
],
bottom: _progress > 0 && _progress < 1
? PreferredSize(
preferredSize: const Size.fromHeight(2),
child: LinearProgressIndicator(
value: _progress,
minHeight: 2,
backgroundColor: Colors.transparent,
),
)
: null,
),
body: _buildBody(cs),
),
);
}
Widget _buildBody(ColorScheme cs) {
if (_loadError != null) {
return _ErrorView(message: _loadError!, onRetry: _load);
}
final launch = _launch;
if (launch == null) {
return const Center(child: CircularProgressIndicator());
}
return InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(launch.url)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
domStorageEnabled: true,
thirdPartyCookiesEnabled: true,
supportZoom: false,
transparentBackground: true,
mediaPlaybackRequiresUserGesture: false,
useHybridComposition: true,
),
onWebViewCreated: (controller) => _controller = controller,
onProgressChanged: (controller, progress) {
if (!mounted) return;
setState(() => _progress = progress / 100);
},
onReceivedError: (controller, request, error) {
if (!mounted) return;
if (request.isForMainFrame ?? false) {
setState(() => _loadError = error.description);
}
},
);
}
}
class _ErrorView extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorView({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Symbols.cloud_off, size: 48, color: cs.onSurfaceVariant),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
),
const SizedBox(height: 24),
FilledButton(
onPressed: onRetry,
child: const Text('Повторить'),
),
],
),
),
);
}
}
+2
View File
@@ -30,6 +30,7 @@ import 'backend/modules/contacts.dart';
import 'backend/modules/file_uploader.dart';
import 'backend/modules/messages.dart';
import 'backend/modules/polls.dart';
import 'backend/modules/webapp.dart';
import 'core/push/push_service.dart';
import 'core/storage/app_database.dart';
import 'core/transport/tls_config.dart';
@@ -47,6 +48,7 @@ final api = Api();
final accountModule = AccountModule(api);
final messagesModule = MessagesModule(api);
final pollsModule = PollsModule(api);
final webAppModule = WebAppModule(api);
final fileUploader = FileUploader(api: api, messages: messagesModule);
final RouteObserver<PageRoute<dynamic>> appRouteObserver =
RouteObserver<PageRoute<dynamic>>();
+64
View File
@@ -326,6 +326,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
flutter_inappwebview_android:
dependency: transitive
description:
name: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
flutter_inappwebview_internal_annotations:
dependency: transitive
description:
name: flutter_inappwebview_internal_annotations
sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6
url: "https://pub.dev"
source: hosted
version: "1.3.0"
flutter_inappwebview_ios:
dependency: transitive
description:
name: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_macos:
dependency: transitive
description:
name: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_platform_interface:
dependency: transitive
description:
name: flutter_inappwebview_platform_interface
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
url: "https://pub.dev"
source: hosted
version: "1.3.0+1"
flutter_inappwebview_web:
dependency: transitive
description:
name: flutter_inappwebview_web
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_windows:
dependency: transitive
description:
name: flutter_inappwebview_windows
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
+1
View File
@@ -64,6 +64,7 @@ dependencies:
firebase_core: ^4.1.1
firebase_messaging: ^16.0.2
flutter_local_notifications: ^21.0.0
flutter_inappwebview: ^6.1.5
dev_dependencies:
flutter_test: