From d988b2c9d3750c071696c78a010730c460447e5c Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 9 Jul 2026 19:46:46 +0700 Subject: [PATCH] fix(downloads): defer verification challenge until app is foregrounded When a download hit a verification challenge while the app was in the background, the browser launch was attempted immediately (blocked or silently dropped by the OS) and the 5-minute grant timeout kept running, so the item failed without the user ever seeing the CAPTCHA. Wait for AppLifecycleState.resumed before opening the challenge and before starting the grant timeout, and post a local notification asking the user to return to the app. If the process is killed while waiting, the persisted queue already restores the item as queued on next launch. --- lib/l10n/app_localizations.dart | 12 +++++++ lib/l10n/app_localizations_ar.dart | 7 ++++ lib/l10n/app_localizations_de.dart | 7 ++++ lib/l10n/app_localizations_en.dart | 7 ++++ lib/l10n/app_localizations_es.dart | 7 ++++ lib/l10n/app_localizations_fr.dart | 7 ++++ lib/l10n/app_localizations_hi.dart | 7 ++++ lib/l10n/app_localizations_id.dart | 7 ++++ lib/l10n/app_localizations_ja.dart | 7 ++++ lib/l10n/app_localizations_ko.dart | 7 ++++ lib/l10n/app_localizations_nl.dart | 7 ++++ lib/l10n/app_localizations_pt.dart | 7 ++++ lib/l10n/app_localizations_ru.dart | 7 ++++ lib/l10n/app_localizations_tr.dart | 7 ++++ lib/l10n/app_localizations_uk.dart | 7 ++++ lib/l10n/app_localizations_zh.dart | 7 ++++ lib/l10n/arb/app_en.arb | 8 +++++ lib/providers/download_queue_provider.dart | 30 ++++++++++++++++ lib/services/notification_service.dart | 40 ++++++++++++++++++++++ 19 files changed, 195 insertions(+) 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;