diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2ca9f616..1fcf6330 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -6213,6 +6213,18 @@ abstract class AppLocalizations { /// **'Downloads Finished ({completed} done, {failed} failed)'** String notifDownloadsFinished(int completed, int failed); + /// Notification title shown when a download needs the user to complete a verification challenge but the app is in the background + /// + /// In en, this message translates to: + /// **'Verification required'** + String get notifVerificationRequiredTitle; + + /// Notification body prompting the user to return to the app to solve a verification challenge + /// + /// In en, this message translates to: + /// **'Open the app to complete verification and resume downloads'** + String get notifVerificationRequiredBody; + /// Notification title when all downloads finish successfully /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 89f15f8e..e796fca3 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsAr extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 161f3d96..76b31491 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3733,6 +3733,13 @@ class AppLocalizationsDe extends AppLocalizations { return 'Downloads abgeschlossen ($completed fertig, $failed fehlgeschlagen)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'Alle Downloads abgeschlossen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d66cd81d..40304918 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsEn extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 17e7acdd..091449b8 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsEs extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index be383e4d..6fdd27dd 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3785,6 +3785,13 @@ class AppLocalizationsFr extends AppLocalizations { return 'Téléchargements terminés ($completed terminé, $failed en échec)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'Tous les téléchargements sont terminés'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 2857a9ae..d3b2735f 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsHi extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 691f6515..d2a96dd1 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -3668,6 +3668,13 @@ class AppLocalizationsId extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index f0aa6aaa..a5384fd4 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3672,6 +3672,13 @@ class AppLocalizationsJa extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3596d031..ae428296 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3670,6 +3670,13 @@ class AppLocalizationsKo extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 1dea8c98..8570aeda 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsNl extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d2f6ae68..aaa10df5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsPt extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c92f3a63..4bba75e0 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3740,6 +3740,13 @@ class AppLocalizationsRu extends AppLocalizations { return 'Загрузка завершена ($completed завершено, $failed не удалось)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'Все загрузки завершены'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index bfa3ca07..d7897b8f 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -3716,6 +3716,13 @@ class AppLocalizationsTr extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 51736a25..867c11fb 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3738,6 +3738,13 @@ class AppLocalizationsUk extends AppLocalizations { return 'Завантаження завершено ($completed завершено, $failed не вдалося)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'Усі завантаження завершено'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 16c45ebb..13ea94c0 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -3685,6 +3685,13 @@ class AppLocalizationsZh extends AppLocalizations { return 'Downloads Finished ($completed done, $failed failed)'; } + @override + String get notifVerificationRequiredTitle => 'Verification required'; + + @override + String get notifVerificationRequiredBody => + 'Open the app to complete verification and resume downloads'; + @override String get notifAllDownloadsComplete => 'All Downloads Complete'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index d928cc70..a7565f47 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4782,6 +4782,14 @@ } } }, + "notifVerificationRequiredTitle": "Verification required", + "@notifVerificationRequiredTitle": { + "description": "Notification title shown when a download needs the user to complete a verification challenge but the app is in the background" + }, + "notifVerificationRequiredBody": "Open the app to complete verification and resume downloads", + "@notifVerificationRequiredBody": { + "description": "Notification body prompting the user to return to the app to solve a verification challenge" + }, "notifAllDownloadsComplete": "All Downloads Complete", "@notifAllDownloadsComplete": { "description": "Notification title when all downloads finish successfully" diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index ec96f1ee..f247827a 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'dart:convert'; import 'dart:io'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -2095,10 +2096,39 @@ class DownloadQueueNotifier extends Notifier { } } + /// 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. + Future _waitForForeground() async { + if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { + return; + } + final completer = Completer(); + final listener = AppLifecycleListener( + onResume: () { + if (!completer.isCompleted) completer.complete(); + }, + ); + try { + await completer.future; + } finally { + listener.dispose(); + } + } + Future _openVerificationAndWait(String extensionId) async { final normalizedExtensionId = extensionId.trim(); if (normalizedExtensionId.isEmpty) return false; + if (WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { + _log.i( + 'Verification required for $normalizedExtensionId while app is in ' + 'background; deferring challenge until the app is foregrounded', + ); + unawaited(_notificationService.showVerificationRequired()); + await _waitForForeground(); + } + final grantEventFuture = PlatformBridge.extensionSessionGrantEvents() .where((event) => event.extensionId == normalizedExtensionId) .first diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index af40dea4..ead2bbc5 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -334,6 +334,46 @@ class NotificationService { ); } + Future showVerificationRequired() async { + if (!_isInitialized) await initialize(); + unawaited(HapticFeedback.mediumImpact()); + + final title = + _l10n?.notifVerificationRequiredTitle ?? 'Verification required'; + final body = + _l10n?.notifVerificationRequiredBody ?? + 'Open the app to complete verification and resume downloads'; + + const androidDetails = AndroidNotificationDetails( + channelId, + channelName, + channelDescription: channelDescription, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + playSound: true, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _showSafely( + id: downloadProgressId, + title: title, + body: body, + details: details, + ); + } + Future showQueueCanceled({required int canceledCount}) async { if (!_isInitialized) await initialize(); if (canceledCount <= 0) return;