feat(webapp): мини-приложения через webAppInitData, вход в Сферум
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>>();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user