Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
263 lines
7.5 KiB
Dart
263 lines
7.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'package:app_links/app_links.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import '../../l10n/app_localizations.dart';
|
|
|
|
import '../../backend/api.dart';
|
|
import '../../frontend/debug/log_export.dart';
|
|
import '../../frontend/screens/digital_id/digital_id_web_screen.dart';
|
|
import '../../frontend/widgets/custom_notification.dart';
|
|
import '../../frontend/widgets/max_link_handler.dart';
|
|
import '../../frontend/widgets/swipe_route.dart';
|
|
import '../../main.dart';
|
|
import '../webpush/max_web_socket.dart';
|
|
import '../webpush/web_push_service.dart';
|
|
import 'desktop_url_scheme.dart';
|
|
import 'max_link.dart';
|
|
|
|
class DeepLinkService {
|
|
DeepLinkService._();
|
|
|
|
static final DeepLinkService instance = DeepLinkService._();
|
|
|
|
final AppLinks _appLinks = AppLinks();
|
|
StreamSubscription<Uri>? _sub;
|
|
StreamSubscription<SessionState>? _stateSub;
|
|
String? _pending;
|
|
bool _pendingLogExport = false;
|
|
String? _pendingExternalCallback;
|
|
WebPushSubscription? _pendingWebPush;
|
|
String? _lastExternalCallback;
|
|
Timer? _externalCallbackRetry;
|
|
Timer? _webPushRetry;
|
|
Timer? _logExportRetry;
|
|
bool _ready = false;
|
|
bool _started = false;
|
|
|
|
Future<void> init() async {
|
|
if (_started) return;
|
|
_started = true;
|
|
|
|
await DesktopUrlScheme.register();
|
|
|
|
_stateSub = api.stateStream.listen((state) {
|
|
if (state == SessionState.online) _flushPending();
|
|
});
|
|
|
|
_sub = _appLinks.uriLinkStream.listen(_onUri);
|
|
try {
|
|
final initial = await _appLinks.getInitialLink();
|
|
if (initial != null) _onUri(initial);
|
|
} catch (_) {}
|
|
}
|
|
|
|
void markReady() {
|
|
_ready = true;
|
|
_flushPending();
|
|
}
|
|
|
|
void handle(Uri uri) => _onUri(uri);
|
|
|
|
void _onUri(Uri uri) {
|
|
if (_isLogExportLink(uri)) {
|
|
_pendingLogExport = true;
|
|
_flushPending();
|
|
return;
|
|
}
|
|
final webPush = _parseWebPushLink(uri);
|
|
if (webPush != null) {
|
|
_pendingWebPush = webPush;
|
|
_flushPending();
|
|
return;
|
|
}
|
|
if (_isExternalCallback(uri)) {
|
|
final callbackUrl = uri.toString();
|
|
if (callbackUrl == _lastExternalCallback) return;
|
|
_lastExternalCallback = callbackUrl;
|
|
_pendingExternalCallback = callbackUrl;
|
|
_flushPending();
|
|
return;
|
|
}
|
|
final url = _normalize(uri);
|
|
if (url == null) return;
|
|
_pending = url;
|
|
_flushPending();
|
|
}
|
|
|
|
void _flushPending() {
|
|
final context = QlyraApp.navigatorKey.currentContext;
|
|
|
|
if (_pendingLogExport) {
|
|
if (context == null) {
|
|
_logExportRetry ??= Timer(const Duration(milliseconds: 300), () {
|
|
_logExportRetry = null;
|
|
_flushPending();
|
|
});
|
|
} else {
|
|
_pendingLogExport = false;
|
|
exportDebugLog(context);
|
|
}
|
|
}
|
|
|
|
if (_pendingExternalCallback != null) {
|
|
if (context == null || api.state != SessionState.online) {
|
|
_externalCallbackRetry ??= Timer(const Duration(milliseconds: 300), () {
|
|
_externalCallbackRetry = null;
|
|
_flushPending();
|
|
});
|
|
} else {
|
|
final url = _pendingExternalCallback!;
|
|
_pendingExternalCallback = null;
|
|
_handleExternalCallback(context, url);
|
|
}
|
|
}
|
|
|
|
if (_pendingWebPush != null) {
|
|
if (context == null) {
|
|
_webPushRetry ??= Timer(const Duration(milliseconds: 300), () {
|
|
_webPushRetry = null;
|
|
_flushPending();
|
|
});
|
|
} else {
|
|
final subscription = _pendingWebPush!;
|
|
_pendingWebPush = null;
|
|
_handleWebPush(context, subscription);
|
|
}
|
|
}
|
|
|
|
if (!_ready || context == null) return;
|
|
final pending = _pending;
|
|
if (pending == null) return;
|
|
final needsConnection = MaxLink.parse(pending)?.needsConnection ?? true;
|
|
if (needsConnection && api.state != SessionState.online) return;
|
|
_pending = null;
|
|
tryHandleMaxLink(context, pending);
|
|
}
|
|
|
|
bool _isExternalCallback(Uri uri) {
|
|
final scheme = uri.scheme.toLowerCase();
|
|
if (scheme != 'https' && scheme != 'http' && scheme != 'max') return false;
|
|
final host = uri.host.toLowerCase();
|
|
if (host != 'max.ru' && host != 'www.max.ru') return false;
|
|
return uri.queryParameters['externalCallback'] == '1';
|
|
}
|
|
|
|
Future<void> _handleExternalCallback(BuildContext context, String url) async {
|
|
try {
|
|
final launch = await webAppModule.handleExternalCallback(url);
|
|
if (!context.mounted) return;
|
|
await pushSwipeable(
|
|
context,
|
|
(_) => DigitalIdWebScreen(initialLaunch: launch),
|
|
);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
showCustomNotification(context, 'Не удалось завершить Цифровой ID: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
WebPushSubscription? _parseWebPushLink(Uri uri) {
|
|
if (!Platform.isIOS) return null;
|
|
final scheme = uri.scheme.toLowerCase();
|
|
if (scheme != 'qlyra') return null;
|
|
|
|
final segments = <String>[
|
|
if (uri.host.isNotEmpty) uri.host,
|
|
...uri.pathSegments,
|
|
].where((s) => s.isNotEmpty).toList();
|
|
if (segments.length != 1 || segments.first != 'webpush') return null;
|
|
|
|
final endpoint = uri.queryParameters['endpoint'] ?? '';
|
|
final publicKey = uri.queryParameters['p256dh'] ?? '';
|
|
final authKey = uri.queryParameters['auth'] ?? '';
|
|
if (endpoint.isEmpty || publicKey.isEmpty || authKey.isEmpty) return null;
|
|
if (Uri.tryParse(endpoint)?.isScheme('https') != true) return null;
|
|
|
|
return WebPushSubscription(
|
|
endpoint: endpoint,
|
|
publicKey: publicKey,
|
|
authKey: authKey,
|
|
);
|
|
}
|
|
|
|
Future<void> _handleWebPush(
|
|
BuildContext context,
|
|
WebPushSubscription subscription,
|
|
) async {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
if (!await WebPushService.instance.isAuthorized()) {
|
|
if (context.mounted) {
|
|
showCustomNotification(context, l10n.webPushNotAuthorized);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await WebPushService.instance.registerSubscription(subscription);
|
|
if (context.mounted) {
|
|
showCustomNotification(context, l10n.webPushLinked);
|
|
}
|
|
} on MaxWebException catch (e) {
|
|
if (context.mounted) {
|
|
showCustomNotification(context, l10n.webPushLinkFailed(e.message));
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
showCustomNotification(context, l10n.webPushLinkFailed('$e'));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool _isLogExportLink(Uri uri) {
|
|
final scheme = uri.scheme.toLowerCase();
|
|
final segments = <String>[
|
|
if (scheme == 'qlyra' && uri.host.isNotEmpty) uri.host,
|
|
...uri.pathSegments,
|
|
].where((s) => s.isNotEmpty).toList();
|
|
|
|
if (scheme == 'qlyra') {
|
|
return segments.length == 1 && segments.first == 'export-logs';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
String? _normalize(Uri uri) {
|
|
final scheme = uri.scheme.toLowerCase();
|
|
|
|
if (scheme == 'https' || scheme == 'http') {
|
|
final host = uri.host.toLowerCase();
|
|
if (host == 'max.ru' || host == 'www.max.ru') return uri.toString();
|
|
return null;
|
|
}
|
|
|
|
if (scheme == 'qlyra' || scheme == 'max') {
|
|
final segments = <String>[
|
|
if (uri.host.isNotEmpty && uri.host.toLowerCase() != 'max.ru') uri.host,
|
|
...uri.pathSegments,
|
|
].where((s) => s.isNotEmpty).toList();
|
|
if (segments.isEmpty) return null;
|
|
final query = uri.query.isNotEmpty ? '?${uri.query}' : '';
|
|
return 'https://max.ru/${segments.join('/')}$query';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
void dispose() {
|
|
_logExportRetry?.cancel();
|
|
_logExportRetry = null;
|
|
_webPushRetry?.cancel();
|
|
_webPushRetry = null;
|
|
_sub?.cancel();
|
|
_sub = null;
|
|
_stateSub?.cancel();
|
|
_stateSub = null;
|
|
_started = false;
|
|
}
|
|
}
|