fix(digital-id): завершать ЕСИА callback через EXTERNAL_CALLBACK
This commit is contained in:
@@ -19,6 +19,41 @@ class WebAppLaunch {
|
||||
const WebAppLaunch({required this.url});
|
||||
}
|
||||
|
||||
class ExternalCallbackResult {
|
||||
final int botId;
|
||||
final String? startParam;
|
||||
|
||||
const ExternalCallbackResult({required this.botId, this.startParam});
|
||||
|
||||
static ExternalCallbackResult? fromPayload(dynamic payload) {
|
||||
final data = _findResponseMap(payload);
|
||||
if (data == null) return null;
|
||||
final botId = _asInt(data['botId'] ?? data['bot_id']);
|
||||
if (botId == null) return null;
|
||||
final startParam = data['startParam'] ?? data['start_param'];
|
||||
return ExternalCallbackResult(
|
||||
botId: botId,
|
||||
startParam: startParam?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
static Map? _findResponseMap(dynamic value) {
|
||||
if (value is! Map) return null;
|
||||
if (value.containsKey('botId') || value.containsKey('bot_id')) return value;
|
||||
for (final key in const ['data', 'result', 'response']) {
|
||||
final nested = _findResponseMap(value[key]);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int? _asInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
class WebAppModule {
|
||||
final Api _api;
|
||||
|
||||
@@ -68,6 +103,26 @@ class WebAppModule {
|
||||
return fetchLaunch(botId);
|
||||
}
|
||||
|
||||
Future<WebAppLaunch> handleExternalCallback(String url) async {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null || uri.queryParameters['externalCallback'] != '1') {
|
||||
throw const WebAppUnavailable('Некорректный callback Цифрового ID');
|
||||
}
|
||||
if (_api.state != SessionState.online) {
|
||||
throw const WebAppUnavailable('Нет соединения с сервером');
|
||||
}
|
||||
final packet = await _api.sendRequest(Opcode.externalCallback, {
|
||||
'url': url,
|
||||
});
|
||||
final result = ExternalCallbackResult.fromPayload(packet.payload);
|
||||
if (result == null) {
|
||||
throw const WebAppUnavailable(
|
||||
'Сервер не вернул данные для завершения Цифрового ID',
|
||||
);
|
||||
}
|
||||
return fetchLaunch(result.botId, startParam: result.startParam);
|
||||
}
|
||||
|
||||
Future<int?> _resolveEntryApp(String key) async {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return null;
|
||||
|
||||
@@ -7,7 +7,7 @@ import '../../../backend/modules/digital_id.dart';
|
||||
import '../../../backend/modules/webapp.dart';
|
||||
import '../../../core/utils/webview_support.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../main.dart' show digitalIdModule;
|
||||
import '../../../main.dart' show digitalIdModule, webAppModule;
|
||||
import '../../../models/digital_id.dart';
|
||||
import '../../widgets/connection_status.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
@@ -123,10 +123,14 @@ class _DigitalIdScreenState extends State<DigitalIdScreen> {
|
||||
builder: (context) => WebAppScreen(
|
||||
title: AppLocalizations.of(context)!.digitalIdGosuslugiTitle,
|
||||
loader: () async => WebAppLaunch(url: link.url),
|
||||
onExternalCallback: webAppModule.handleExternalCallback,
|
||||
closeAfterExternalCallback: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
await digitalIdModule.loadDocuments(createIfMissing: true);
|
||||
if (!mounted) return;
|
||||
await _load();
|
||||
} on DigitalIdException catch (e) {
|
||||
if (mounted) showCustomNotification(context, e.message);
|
||||
|
||||
@@ -186,6 +186,7 @@ class DigitalIdWebScreen extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
onExternalCallback: webAppModule.handleExternalCallback,
|
||||
onConsoleMessage: kDebugMode
|
||||
? (controller, consoleMessage) {
|
||||
debugPrint('[KOMET-DID] ${consoleMessage.message}');
|
||||
@@ -199,7 +200,7 @@ class DigitalIdWebScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
: null,
|
||||
shouldOverrideUrlLoading: (controller, action, currentUrl) async {
|
||||
shouldOverrideUrlLoading: (_, action, _) async {
|
||||
final uri = action.request.url;
|
||||
final url = uri?.toString() ?? '';
|
||||
final scheme = uri?.scheme ?? '';
|
||||
@@ -208,19 +209,7 @@ class DigitalIdWebScreen extends StatelessWidget {
|
||||
'[KOMET-DID] nav: ${url.length > 140 ? url.substring(0, 140) : url}',
|
||||
);
|
||||
}
|
||||
final isCallback =
|
||||
url.contains('?externalCallback=') ||
|
||||
url.contains('&externalCallback=');
|
||||
if (isCallback || (scheme != 'http' && scheme != 'https')) {
|
||||
final launchUrl = currentUrl ?? 'https://digital-id.max.ru';
|
||||
final hashIdx = launchUrl.indexOf('#');
|
||||
final base = hashIdx >= 0
|
||||
? launchUrl.substring(0, hashIdx)
|
||||
: launchUrl;
|
||||
final frag = hashIdx >= 0 ? launchUrl.substring(hashIdx) : '';
|
||||
final query = uri?.query ?? '';
|
||||
final target = query.isEmpty ? launchUrl : '$base?$query$frag';
|
||||
controller.loadUrl(urlRequest: URLRequest(url: WebUri(target)));
|
||||
if (scheme != 'http' && scheme != 'https') {
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
|
||||
@@ -24,6 +24,8 @@ class WebAppScreen extends StatefulWidget {
|
||||
onConsoleMessage;
|
||||
final void Function(InAppWebViewController controller, WebUri? url)?
|
||||
onLoadStart;
|
||||
final Future<WebAppLaunch> Function(String url)? onExternalCallback;
|
||||
final bool closeAfterExternalCallback;
|
||||
final Future<NavigationActionPolicy?> Function(
|
||||
InAppWebViewController controller,
|
||||
NavigationAction navigationAction,
|
||||
@@ -39,6 +41,8 @@ class WebAppScreen extends StatefulWidget {
|
||||
this.onWebViewCreated,
|
||||
this.onConsoleMessage,
|
||||
this.onLoadStart,
|
||||
this.onExternalCallback,
|
||||
this.closeAfterExternalCallback = false,
|
||||
this.shouldOverrideUrlLoading,
|
||||
});
|
||||
|
||||
@@ -67,7 +71,8 @@ class _WebAppScreenState extends State<WebAppScreen> {
|
||||
try {
|
||||
// Тот же UA, что уходит в sessionInit (из handshake-устройства ядра),
|
||||
// чтобы веб-аппы видели нативный клиент; фолбэк — браузерный UA спуфа.
|
||||
_userAgent = api.session?.userAgent() ??
|
||||
_userAgent =
|
||||
api.session?.userAgent() ??
|
||||
await SpoofingService.getWebViewUserAgent() ??
|
||||
'';
|
||||
final launch = await widget.loader();
|
||||
@@ -88,6 +93,37 @@ class _WebAppScreenState extends State<WebAppScreen> {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<NavigationActionPolicy?> _handleNavigation(
|
||||
InAppWebViewController controller,
|
||||
NavigationAction action,
|
||||
) async {
|
||||
final uri = action.request.url;
|
||||
final callback = widget.onExternalCallback;
|
||||
if (callback != null && uri?.queryParameters['externalCallback'] == '1') {
|
||||
try {
|
||||
final launch = await callback(uri.toString());
|
||||
if (!mounted) return NavigationActionPolicy.CANCEL;
|
||||
if (widget.closeAfterExternalCallback) {
|
||||
Navigator.of(context).pop(launch);
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
setState(() {
|
||||
_launch = launch;
|
||||
_loadError = null;
|
||||
});
|
||||
await controller.loadUrl(
|
||||
urlRequest: URLRequest(url: WebUri(launch.url)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _loadError = e.toString());
|
||||
}
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
final handler = widget.shouldOverrideUrlLoading;
|
||||
if (handler == null) return NavigationActionPolicy.ALLOW;
|
||||
return handler(controller, action, _launch?.url);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
@@ -153,7 +189,9 @@ class _WebAppScreenState extends State<WebAppScreen> {
|
||||
transparentBackground: true,
|
||||
mediaPlaybackRequiresUserGesture: false,
|
||||
useHybridComposition: true,
|
||||
useShouldOverrideUrlLoading: widget.shouldOverrideUrlLoading != null,
|
||||
useShouldOverrideUrlLoading:
|
||||
widget.shouldOverrideUrlLoading != null ||
|
||||
widget.onExternalCallback != null,
|
||||
userAgent: _userAgent,
|
||||
),
|
||||
onWebViewCreated: (controller) {
|
||||
@@ -164,13 +202,11 @@ class _WebAppScreenState extends State<WebAppScreen> {
|
||||
askWebViewPermission(context, request),
|
||||
onConsoleMessage: widget.onConsoleMessage,
|
||||
onLoadStart: widget.onLoadStart,
|
||||
shouldOverrideUrlLoading: widget.shouldOverrideUrlLoading == null
|
||||
shouldOverrideUrlLoading:
|
||||
widget.shouldOverrideUrlLoading == null &&
|
||||
widget.onExternalCallback == null
|
||||
? null
|
||||
: (controller, action) => widget.shouldOverrideUrlLoading!(
|
||||
controller,
|
||||
action,
|
||||
launch.url,
|
||||
),
|
||||
: _handleNavigation,
|
||||
onProgressChanged: (controller, progress) {
|
||||
if (!mounted) return;
|
||||
setState(() => _progress = progress / 100);
|
||||
|
||||
Reference in New Issue
Block a user