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 '../../widgets/sheet_helpers.dart';
|
||||||
import '../auth/login_screen.dart';
|
import '../auth/login_screen.dart';
|
||||||
import '../auth/proxy_settings_sheet.dart';
|
import '../auth/proxy_settings_sheet.dart';
|
||||||
|
import '../webapp/web_app_screen.dart';
|
||||||
import 'cloud_storage_screen.dart';
|
import 'cloud_storage_screen.dart';
|
||||||
import 'customization_screen.dart';
|
import 'customization_screen.dart';
|
||||||
import 'performance_screen.dart';
|
import 'performance_screen.dart';
|
||||||
@@ -252,9 +253,20 @@ class _SettingsTabState extends State<SettingsTab> {
|
|||||||
icon: Symbols.badge,
|
icon: Symbols.badge,
|
||||||
label: 'Цифровой ID',
|
label: 'Цифровой ID',
|
||||||
),
|
),
|
||||||
const _SettingsItem(
|
_SettingsItem(
|
||||||
icon: Symbols.language,
|
icon: Symbols.language,
|
||||||
label: 'Войти в Сферум',
|
label: 'Войти в Сферум',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => WebAppScreen(
|
||||||
|
title: 'Сферум',
|
||||||
|
loader: () => webAppModule.fetchSferum(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_SettingsItem(
|
_SettingsItem(
|
||||||
icon: Symbols.info,
|
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/file_uploader.dart';
|
||||||
import 'backend/modules/messages.dart';
|
import 'backend/modules/messages.dart';
|
||||||
import 'backend/modules/polls.dart';
|
import 'backend/modules/polls.dart';
|
||||||
|
import 'backend/modules/webapp.dart';
|
||||||
import 'core/push/push_service.dart';
|
import 'core/push/push_service.dart';
|
||||||
import 'core/storage/app_database.dart';
|
import 'core/storage/app_database.dart';
|
||||||
import 'core/transport/tls_config.dart';
|
import 'core/transport/tls_config.dart';
|
||||||
@@ -47,6 +48,7 @@ final api = Api();
|
|||||||
final accountModule = AccountModule(api);
|
final accountModule = AccountModule(api);
|
||||||
final messagesModule = MessagesModule(api);
|
final messagesModule = MessagesModule(api);
|
||||||
final pollsModule = PollsModule(api);
|
final pollsModule = PollsModule(api);
|
||||||
|
final webAppModule = WebAppModule(api);
|
||||||
final fileUploader = FileUploader(api: api, messages: messagesModule);
|
final fileUploader = FileUploader(api: api, messages: messagesModule);
|
||||||
final RouteObserver<PageRoute<dynamic>> appRouteObserver =
|
final RouteObserver<PageRoute<dynamic>> appRouteObserver =
|
||||||
RouteObserver<PageRoute<dynamic>>();
|
RouteObserver<PageRoute<dynamic>>();
|
||||||
|
|||||||
@@ -326,6 +326,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
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:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ dependencies:
|
|||||||
firebase_core: ^4.1.1
|
firebase_core: ^4.1.1
|
||||||
firebase_messaging: ^16.0.2
|
firebase_messaging: ^16.0.2
|
||||||
flutter_local_notifications: ^21.0.0
|
flutter_local_notifications: ^21.0.0
|
||||||
|
flutter_inappwebview: ^6.1.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user