mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-22 01:20:47 +02:00
fix(download): route verification notifications to pending challenges
This commit is contained in:
@@ -6470,6 +6470,15 @@
|
||||
"@extensionVerificationBrowserInApp": {
|
||||
"description": "Chip label for in-app browser verification mode"
|
||||
},
|
||||
"extensionVerificationUnavailable": "No verification page is available for {extension}. Retry the download to request a new challenge.",
|
||||
"@extensionVerificationUnavailable": {
|
||||
"description": "Shown when the app cannot retrieve a pending verification challenge",
|
||||
"placeholders": {
|
||||
"extension": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionVerificationHelpTitleManual": "Open verification manually",
|
||||
"@extensionVerificationHelpTitleManual": {
|
||||
"description": "Dialog title when automatic browser launch for verification fails"
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:spotiflac_android/services/app_state_database.dart';
|
||||
import 'package:spotiflac_android/services/extension_storage_service.dart';
|
||||
import 'package:spotiflac_android/utils/local_library_scan_prefs.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
|
||||
final _log = AppLogger('Main');
|
||||
|
||||
@@ -235,6 +236,7 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
|
||||
Timer? _localLibraryWarmupTimer;
|
||||
bool _localLibraryWarmupScheduled = false;
|
||||
bool _autoScanTriggeredOnLaunch = false;
|
||||
StreamSubscription<void>? _verificationNotificationSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -242,6 +244,10 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_verificationNotificationSubscription =
|
||||
PlatformBridge.verificationNotificationEvents().listen(
|
||||
(_) => unawaited(_consumeVerificationNotification()),
|
||||
);
|
||||
_initializeAppServices();
|
||||
_initializeExtensions();
|
||||
_initializeDeferredProviders();
|
||||
@@ -255,12 +261,15 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
|
||||
_localLibraryEnabledSub?.close();
|
||||
_downloadHistoryWarmupTimer?.cancel();
|
||||
_localLibraryWarmupTimer?.cancel();
|
||||
_verificationNotificationSubscription?.cancel();
|
||||
NotificationService().verificationNotifications.setHandler(null);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
unawaited(_consumeVerificationNotification());
|
||||
CoverCacheManager.scheduleMaintenance();
|
||||
_maybeAutoScanLocalLibrary();
|
||||
if (ref.exists(localLibraryProvider)) {
|
||||
@@ -414,11 +423,38 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
|
||||
storage.dataDir,
|
||||
masterKey: storage.masterKey,
|
||||
);
|
||||
if (!mounted) return;
|
||||
NotificationService().verificationNotifications.setHandler((
|
||||
target,
|
||||
) async {
|
||||
if (!mounted) return;
|
||||
try {
|
||||
await ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.handleVerificationNotificationTap(target);
|
||||
} catch (error) {
|
||||
_log.w('Could not open verification notification: $error');
|
||||
if (mounted) showExtensionVerificationUnavailable(target.extensionId);
|
||||
}
|
||||
});
|
||||
await _consumeVerificationNotification();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to initialize extensions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _consumeVerificationNotification() async {
|
||||
if (!mounted || !Platform.isAndroid) return;
|
||||
try {
|
||||
final payload = await PlatformBridge.consumeVerificationNotification();
|
||||
if (mounted) {
|
||||
NotificationService().verificationNotifications.receive(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
_log.w('Could not read pending verification notification: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/services/download_request_payload.dart';
|
||||
import 'package:spotiflac_android/services/ffmpeg_service.dart';
|
||||
import 'package:spotiflac_android/services/notification_service.dart';
|
||||
import 'package:spotiflac_android/services/verification_notification.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart' hide log;
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
@@ -32,6 +33,7 @@ import 'package:spotiflac_android/utils/audio_format_utils.dart';
|
||||
import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
|
||||
import 'package:spotiflac_android/utils/int_utils.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
import 'package:spotiflac_android/utils/download_error_type.dart';
|
||||
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
|
||||
import 'package:spotiflac_android/utils/progress_stream_poller.dart';
|
||||
|
||||
@@ -363,6 +365,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
int _failedInSession = 0;
|
||||
int _queueItemSequence = 0;
|
||||
bool _isLoaded = false;
|
||||
final Completer<void> _queueRestored = Completer<void>();
|
||||
bool _foregroundResumeScheduled = false;
|
||||
bool _iosBackgroundExecutionExpired = false;
|
||||
StreamSubscription<List<String>>? _iosBackgroundExpirationSubscription;
|
||||
@@ -446,6 +449,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
if (!_queueRestored.isCompleted) _queueRestored.complete();
|
||||
_verificationWaitCoordinator.cancelAll();
|
||||
_progressPoller.stop();
|
||||
_connectivitySub?.cancel();
|
||||
@@ -460,9 +464,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
});
|
||||
|
||||
Future.microtask(() async {
|
||||
updateSettings(ref.read(settingsProvider));
|
||||
await _initOutputDir();
|
||||
await _loadQueueFromStorage();
|
||||
try {
|
||||
updateSettings(ref.read(settingsProvider));
|
||||
await _initOutputDir();
|
||||
await _loadQueueFromStorage();
|
||||
} finally {
|
||||
if (!_queueRestored.isCompleted) _queueRestored.complete();
|
||||
}
|
||||
});
|
||||
return const DownloadQueueState();
|
||||
}
|
||||
@@ -525,6 +533,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handleVerificationNotificationTap(
|
||||
VerificationNotification target,
|
||||
) => _handleVerificationNotificationTap(target);
|
||||
|
||||
void _handleIosBackgroundDownloadExpiration(List<String> nativeItemIds) {
|
||||
if (!Platform.isIOS) return;
|
||||
final cancelledItemIds = nativeItemIds.toSet();
|
||||
@@ -1415,6 +1427,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
item.status != DownloadStatus.skipped) {
|
||||
return item;
|
||||
}
|
||||
_verificationRetryGuard.clearItem(item.id);
|
||||
_rateLimitRetriedItemIds.remove(item.id);
|
||||
return item.copyWith(
|
||||
status: DownloadStatus.queued,
|
||||
progress: 0,
|
||||
@@ -1571,23 +1585,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
}
|
||||
|
||||
DownloadErrorType _downloadErrorTypeFromBackend(String? errorType) {
|
||||
switch (errorType) {
|
||||
case 'not_found':
|
||||
return DownloadErrorType.notFound;
|
||||
case 'rate_limit':
|
||||
return DownloadErrorType.rateLimit;
|
||||
case 'network':
|
||||
return DownloadErrorType.network;
|
||||
case 'permission':
|
||||
return DownloadErrorType.permission;
|
||||
case 'verification_required':
|
||||
return DownloadErrorType.verificationRequired;
|
||||
default:
|
||||
return DownloadErrorType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
DownloadErrorType _downloadErrorTypeFromMessage(String errorMsg) {
|
||||
final lowerMsg = errorMsg.toLowerCase();
|
||||
if (isExtensionVerificationRequired(errorMsg)) {
|
||||
|
||||
@@ -1106,13 +1106,12 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
? (resultMap?['error']?.toString() ?? 'Download failed')
|
||||
: error;
|
||||
final backendErrorType = resultMap == null
|
||||
? DownloadErrorType.unknown
|
||||
: _downloadErrorTypeFromBackend(
|
||||
? null
|
||||
: downloadErrorTypeFromBackend(
|
||||
resultMap['error_type']?.toString(),
|
||||
);
|
||||
final errorType = backendErrorType == DownloadErrorType.unknown
|
||||
? _downloadErrorTypeFromMessage(errorMsg)
|
||||
: backendErrorType;
|
||||
final errorType =
|
||||
backendErrorType ?? _downloadErrorTypeFromMessage(errorMsg);
|
||||
try {
|
||||
if (await _recoverNativeWorkerStorageFailure(
|
||||
context: context,
|
||||
|
||||
@@ -1873,10 +1873,9 @@ class _DownloadRun {
|
||||
return false;
|
||||
}
|
||||
|
||||
final backendErrorType = n._downloadErrorTypeFromBackend(errorTypeStr);
|
||||
final errorType = backendErrorType == DownloadErrorType.unknown
|
||||
? n._downloadErrorTypeFromMessage(errorMsg)
|
||||
: backendErrorType;
|
||||
final backendErrorType = downloadErrorTypeFromBackend(errorTypeStr);
|
||||
final errorType =
|
||||
backendErrorType ?? n._downloadErrorTypeFromMessage(errorMsg);
|
||||
|
||||
if (errorType == DownloadErrorType.verificationRequired) {
|
||||
await n._handleVerificationRequiredDownload(
|
||||
|
||||
@@ -2,6 +2,41 @@
|
||||
part of 'download_queue_provider.dart';
|
||||
|
||||
extension _DownloadQueueVerificationGate on DownloadQueueNotifier {
|
||||
Future<void> _handleVerificationNotificationTap(
|
||||
VerificationNotification target,
|
||||
) async {
|
||||
await _queueRestored.future;
|
||||
if (!ref.mounted) return;
|
||||
final item = _findItemById(target.itemId);
|
||||
if (item != null &&
|
||||
(item.status == DownloadStatus.completed ||
|
||||
item.status == DownloadStatus.skipped ||
|
||||
_isLocallyCancelled(item.id, item: item))) {
|
||||
return;
|
||||
}
|
||||
// Foregrounding wakes an existing queue waiter. Do not replace it or
|
||||
// reopen a browser that the queue already owns.
|
||||
if ((item != null &&
|
||||
_verificationWaitCoordinator.hasActiveWaiter(item.id)) ||
|
||||
_verificationWaitCoordinator.hasActiveFlow(target.extensionId)) {
|
||||
return;
|
||||
}
|
||||
// Failed items may not survive queue restoration. The notification still
|
||||
// identifies the owning extension, but cannot restart a missing item.
|
||||
final verified = await _openVerificationAndWait(
|
||||
item?.id ?? 'notification:${target.tapId}',
|
||||
target.extensionId,
|
||||
);
|
||||
if (!ref.mounted || !verified || state.isPaused) return;
|
||||
final current = _findItemById(target.itemId);
|
||||
if (current != null &&
|
||||
current.status == DownloadStatus.failed &&
|
||||
current.errorType == DownloadErrorType.verificationRequired &&
|
||||
!_isLocallyCancelled(current.id, item: current)) {
|
||||
await retryItem(current.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes when the app is in the foreground. Verification challenges
|
||||
/// can only be handled there: launching a browser from the background is
|
||||
/// blocked by the OS and the challenge would expire unseen.
|
||||
@@ -97,7 +132,10 @@ extension _DownloadQueueVerificationGate on DownloadQueueNotifier {
|
||||
);
|
||||
|
||||
try {
|
||||
await _notificationService.showVerificationRequired();
|
||||
await _notificationService.showVerificationRequired(
|
||||
extensionId: targetService,
|
||||
itemId: item.id,
|
||||
);
|
||||
} catch (error) {
|
||||
_log.w('Failed to show the verification-required notification: $error');
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ class DownloadVerificationWaitCoordinator {
|
||||
bool hasActiveFlow(String service) =>
|
||||
_flowsByService.containsKey(_serviceKey(service));
|
||||
|
||||
bool hasActiveWaiter(String itemId) =>
|
||||
_cancellationsByItem.containsKey(itemId);
|
||||
|
||||
Future<bool> waitForGrant({
|
||||
required String itemId,
|
||||
required String service,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
import 'package:spotiflac_android/services/verification_notification.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
@@ -15,6 +16,8 @@ class NotificationService {
|
||||
final FlutterLocalNotificationsPlugin _notifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
bool _isInitialized = false;
|
||||
Future<void>? _initialization;
|
||||
final verificationNotifications = VerificationNotificationRouter();
|
||||
bool _notificationPermissionRequested = false;
|
||||
AppLocalizations? _l10n;
|
||||
|
||||
@@ -43,9 +46,14 @@ class NotificationService {
|
||||
static const String libraryChannelDescription =
|
||||
'Shows local library scan progress';
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
Future<void> initialize() {
|
||||
if (_isInitialized) return Future.value();
|
||||
return _initialization ??= _initialize().whenComplete(() {
|
||||
_initialization = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
const androidSettings = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
);
|
||||
@@ -60,7 +68,16 @@ class NotificationService {
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await _notifications.initialize(settings: initSettings);
|
||||
await _notifications.initialize(
|
||||
settings: initSettings,
|
||||
onDidReceiveNotificationResponse: (response) {
|
||||
verificationNotifications.receive(response.payload);
|
||||
},
|
||||
);
|
||||
final launch = await _notifications.getNotificationAppLaunchDetails();
|
||||
if (launch?.didNotificationLaunchApp == true) {
|
||||
verificationNotifications.receive(launch?.notificationResponse?.payload);
|
||||
}
|
||||
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||
final androidImpl = _notifications
|
||||
@@ -126,6 +143,7 @@ class NotificationService {
|
||||
required String title,
|
||||
required String body,
|
||||
required NotificationDetails details,
|
||||
String? payload,
|
||||
}) async {
|
||||
if (!await _ensureNotificationPermission()) return;
|
||||
|
||||
@@ -135,6 +153,7 @@ class NotificationService {
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: details,
|
||||
payload: payload,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
final isNotificationsNotAllowed =
|
||||
@@ -305,7 +324,10 @@ class NotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showVerificationRequired() async {
|
||||
Future<void> showVerificationRequired({
|
||||
required String extensionId,
|
||||
required String itemId,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
unawaited(HapticFeedback.mediumImpact());
|
||||
|
||||
@@ -319,6 +341,11 @@ class NotificationService {
|
||||
id: verificationRequiredId,
|
||||
title: title,
|
||||
body: body,
|
||||
payload: VerificationNotification(
|
||||
extensionId: extensionId,
|
||||
itemId: itemId,
|
||||
tapId: 'dart:${DateTime.now().microsecondsSinceEpoch}',
|
||||
).encode(),
|
||||
details: _details(
|
||||
playSound: true,
|
||||
presentBadge: true,
|
||||
|
||||
@@ -203,6 +203,8 @@ class PlatformBridge {
|
||||
StreamController<ExtensionSessionGrantEvent>.broadcast();
|
||||
static final StreamController<void> _libraryStorageEvents =
|
||||
StreamController<void>.broadcast();
|
||||
static final StreamController<void> _verificationNotificationEvents =
|
||||
StreamController<void>.broadcast();
|
||||
static final StreamController<List<String>>
|
||||
_iosBackgroundDownloadExpirationEvents =
|
||||
StreamController<List<String>>.broadcast();
|
||||
@@ -223,6 +225,14 @@ class PlatformBridge {
|
||||
return _libraryStorageEvents.stream;
|
||||
}
|
||||
|
||||
static Stream<void> verificationNotificationEvents() {
|
||||
_ensureBackendEventHandler();
|
||||
return _verificationNotificationEvents.stream;
|
||||
}
|
||||
|
||||
static Future<String?> consumeVerificationNotification() =>
|
||||
_channel.invokeMethod<String>('consumeVerificationNotification');
|
||||
|
||||
static Stream<List<String>> iosBackgroundDownloadExpirationEvents() {
|
||||
_ensureBackendEventHandler();
|
||||
return _iosBackgroundDownloadExpirationEvents.stream;
|
||||
@@ -250,6 +260,9 @@ class PlatformBridge {
|
||||
case 'libraryStorageChanged':
|
||||
_libraryStorageEvents.add(null);
|
||||
return null;
|
||||
case 'extensionVerificationNotificationTapped':
|
||||
_verificationNotificationEvents.add(null);
|
||||
return null;
|
||||
case 'iosBackgroundDownloadExpired':
|
||||
final raw = call.arguments;
|
||||
var itemIds = const <String>[];
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
class VerificationNotification {
|
||||
const VerificationNotification({
|
||||
required this.extensionId,
|
||||
required this.itemId,
|
||||
required this.tapId,
|
||||
});
|
||||
|
||||
final String extensionId;
|
||||
final String itemId;
|
||||
final String tapId;
|
||||
|
||||
String encode() => jsonEncode({
|
||||
'kind': 'extension_verification',
|
||||
'extension_id': extensionId,
|
||||
'item_id': itemId,
|
||||
'tap_id': tapId,
|
||||
});
|
||||
|
||||
static VerificationNotification? parse(Object? payload) {
|
||||
try {
|
||||
final value = payload is String ? jsonDecode(payload) : payload;
|
||||
if (value is! Map || value['kind'] != 'extension_verification') {
|
||||
return null;
|
||||
}
|
||||
final extensionId = value['extension_id'];
|
||||
final itemId = value['item_id'];
|
||||
final tapId = value['tap_id'];
|
||||
if (extensionId is! String ||
|
||||
extensionId.trim().isEmpty ||
|
||||
itemId is! String ||
|
||||
itemId.trim().isEmpty ||
|
||||
tapId is! String ||
|
||||
tapId.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return VerificationNotification(
|
||||
extensionId: extensionId.trim(),
|
||||
itemId: itemId.trim(),
|
||||
tapId: tapId.trim(),
|
||||
);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds cold-start taps until extensions and queue restoration are ready.
|
||||
/// A repeated native/plugin delivery must not launch a second challenge.
|
||||
class VerificationNotificationRouter {
|
||||
final Map<String, VerificationNotification> _pending = {};
|
||||
final Set<String> _handled = {};
|
||||
final Set<String> _active = {};
|
||||
Future<void> Function(VerificationNotification)? _handler;
|
||||
|
||||
void setHandler(Future<void> Function(VerificationNotification)? handler) {
|
||||
_handler = handler;
|
||||
_drain();
|
||||
}
|
||||
|
||||
void receive(Object? payload) {
|
||||
final target = VerificationNotification.parse(payload);
|
||||
if (target == null ||
|
||||
_handled.contains(target.tapId) ||
|
||||
_active.contains(target.tapId)) {
|
||||
return;
|
||||
}
|
||||
_pending.putIfAbsent(target.tapId, () => target);
|
||||
_drain();
|
||||
}
|
||||
|
||||
void _drain() {
|
||||
final handler = _handler;
|
||||
if (handler == null) return;
|
||||
for (final target in _pending.values.toList()) {
|
||||
_pending.remove(target.tapId);
|
||||
_active.add(target.tapId);
|
||||
// The queue deduplicates flows per extension. A different provider's
|
||||
// notification must not wait behind an unrelated grant timeout.
|
||||
unawaited(
|
||||
Future<void>.sync(() => handler(target)).whenComplete(() {
|
||||
_active.remove(target.tapId);
|
||||
_handled.add(target.tapId);
|
||||
if (_handled.length > 32) _handled.remove(_handled.first);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
|
||||
/// Null permits legacy message inference; unknown preserves a typed failure's
|
||||
/// original message without mistaking provider authentication for a challenge.
|
||||
DownloadErrorType? downloadErrorTypeFromBackend(String? errorType) {
|
||||
switch (errorType?.trim().toLowerCase()) {
|
||||
case 'not_found':
|
||||
return DownloadErrorType.notFound;
|
||||
case 'rate_limit':
|
||||
return DownloadErrorType.rateLimit;
|
||||
case 'network':
|
||||
return DownloadErrorType.network;
|
||||
case 'permission':
|
||||
return DownloadErrorType.permission;
|
||||
case 'verification_required':
|
||||
return DownloadErrorType.verificationRequired;
|
||||
case 'authentication_error':
|
||||
case 'provider_auth_failed':
|
||||
case 'request_auth_invalid':
|
||||
case 'provider_reauth_required':
|
||||
return DownloadErrorType.unknown;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,7 @@ bool isExtensionVerificationRequired(Object error) {
|
||||
message.contains('needsverification') ||
|
||||
message.contains('needs verification') ||
|
||||
message.contains('session is not authenticated') ||
|
||||
message.contains('unauthorized') ||
|
||||
message.contains('precondition required') ||
|
||||
_containsHttpStatusCode(message, '401') ||
|
||||
_containsHttpStatusCode(message, '428');
|
||||
message.contains('signed session expired');
|
||||
}
|
||||
|
||||
Future<T> runExtensionOperationWithVerificationRetry<T>({
|
||||
@@ -82,15 +79,6 @@ String? extensionIdFromVerificationError(
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _containsHttpStatusCode(String message, String code) {
|
||||
return message.contains('http $code') ||
|
||||
message.contains('http status $code') ||
|
||||
message.contains('status $code') ||
|
||||
message.contains('$code for ') ||
|
||||
message.contains('$code:') ||
|
||||
message.contains('$code;');
|
||||
}
|
||||
|
||||
Future<bool> openPendingExtensionVerification(
|
||||
String extensionId, {
|
||||
String browserMode = 'in_app_first',
|
||||
@@ -100,14 +88,22 @@ Future<bool> openPendingExtensionVerification(
|
||||
final normalizedExtensionId = extensionId.trim();
|
||||
if (normalizedExtensionId.isEmpty) return false;
|
||||
|
||||
var cancelled = false;
|
||||
if (cancellationSignal != null) {
|
||||
unawaited(cancellationSignal.then((_) => cancelled = true));
|
||||
}
|
||||
try {
|
||||
final pending = await _awaitVerificationStepOrCancellation(
|
||||
PlatformBridge.getExtensionPendingAuth(normalizedExtensionId),
|
||||
cancellationSignal,
|
||||
);
|
||||
if (pending == null) return false;
|
||||
final authUrl = pending['auth_url']?.toString().trim() ?? '';
|
||||
if (authUrl.isEmpty) return false;
|
||||
if (cancelled) return false;
|
||||
final authUrl = pending?['auth_url']?.toString().trim() ?? '';
|
||||
if (authUrl.isEmpty) {
|
||||
_log.w('No pending verification challenge for $normalizedExtensionId');
|
||||
showExtensionVerificationUnavailable(normalizedExtensionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
final uri = Uri.tryParse(authUrl);
|
||||
if (uri == null) return false;
|
||||
@@ -138,10 +134,22 @@ Future<bool> openPendingExtensionVerification(
|
||||
_log.w(
|
||||
'Failed to open verification challenge for $normalizedExtensionId: $e',
|
||||
);
|
||||
if (!cancelled) showExtensionVerificationUnavailable(normalizedExtensionId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void showExtensionVerificationUnavailable(String extensionId) {
|
||||
final context = AppNavigationService.rootNavigatorKey.currentContext;
|
||||
if (context == null || !context.mounted) return;
|
||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.extensionVerificationUnavailable(extensionId)),
|
||||
duration: const Duration(seconds: 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Timer? scheduleExtensionVerificationHelpDialog(
|
||||
String extensionId,
|
||||
Uri? authUri, {
|
||||
|
||||
Reference in New Issue
Block a user