mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-11 10:18:44 +02:00
feat(extensions): manual verification help when browser launch fails
Expose a root navigator for global dialogs, show a fallback help sheet with copy and reopen actions when verification URLs cannot launch, and schedule the same prompt after a timeout during pending grants.
This commit is contained in:
1 parent
4d6f7d8b08
commit
dcfd95f276
5 files changed
+180
-22
No files matched your search
@@ -7,6 +7,7 @@ import 'package:spotiflac_android/screens/main_shell.dart';
|
|||||||
import 'package:spotiflac_android/screens/setup_screen.dart';
|
import 'package:spotiflac_android/screens/setup_screen.dart';
|
||||||
import 'package:spotiflac_android/screens/tutorial_screen.dart';
|
import 'package:spotiflac_android/screens/tutorial_screen.dart';
|
||||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||||
|
import 'package:spotiflac_android/services/app_navigation_service.dart';
|
||||||
import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
|
import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
|
||||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ final _routerProvider = Provider<GoRouter>((ref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
|
navigatorKey: AppNavigationService.rootNavigatorKey,
|
||||||
initialLocation: initialLocation,
|
initialLocation: initialLocation,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: '/', builder: (context, state) => const MainShell()),
|
GoRoute(path: '/', builder: (context, state) => const MainShell()),
|
||||||
|
|||||||
@@ -2096,14 +2096,31 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final opened = await openPendingExtensionVerification(
|
final browserMode = ref
|
||||||
normalizedExtensionId,
|
.read(settingsProvider)
|
||||||
browserMode: ref.read(settingsProvider).extensionVerificationBrowserMode,
|
.extensionVerificationBrowserMode;
|
||||||
);
|
Uri? authUri;
|
||||||
if (!opened) return false;
|
Timer? helpDialogTimer;
|
||||||
|
|
||||||
final event = await grantEventFuture;
|
try {
|
||||||
return event.success;
|
final opened = await openPendingExtensionVerification(
|
||||||
|
normalizedExtensionId,
|
||||||
|
browserMode: browserMode,
|
||||||
|
onAuthUri: (uri) => authUri = uri,
|
||||||
|
);
|
||||||
|
if (!opened) return false;
|
||||||
|
|
||||||
|
helpDialogTimer = scheduleExtensionVerificationHelpDialog(
|
||||||
|
normalizedExtensionId,
|
||||||
|
authUri,
|
||||||
|
browserMode: browserMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
final event = await grantEventFuture;
|
||||||
|
return event.success;
|
||||||
|
} finally {
|
||||||
|
helpDialogTimer?.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _handleVerificationRequiredDownload(
|
Future<bool> _handleVerificationRequiredDownload(
|
||||||
|
|||||||
@@ -683,15 +683,26 @@ class TrackNotifier extends Notifier<TrackState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final browserMode = ref
|
||||||
|
.read(settingsProvider)
|
||||||
|
.extensionVerificationBrowserMode;
|
||||||
|
Uri? authUri;
|
||||||
|
Timer? helpDialogTimer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final opened = await openPendingExtensionVerification(
|
final opened = await openPendingExtensionVerification(
|
||||||
normalizedExtensionId,
|
normalizedExtensionId,
|
||||||
browserMode: ref
|
browserMode: browserMode,
|
||||||
.read(settingsProvider)
|
onAuthUri: (uri) => authUri = uri,
|
||||||
.extensionVerificationBrowserMode,
|
|
||||||
);
|
);
|
||||||
if (!opened) return false;
|
if (!opened) return false;
|
||||||
|
|
||||||
|
helpDialogTimer = scheduleExtensionVerificationHelpDialog(
|
||||||
|
normalizedExtensionId,
|
||||||
|
authUri,
|
||||||
|
browserMode: browserMode,
|
||||||
|
);
|
||||||
|
|
||||||
final event = await grantCompleter.future.timeout(
|
final event = await grantCompleter.future.timeout(
|
||||||
const Duration(minutes: 5),
|
const Duration(minutes: 5),
|
||||||
);
|
);
|
||||||
@@ -702,6 +713,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
|
helpDialogTimer?.cancel();
|
||||||
await grantSub.cancel();
|
await grantSub.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppNavigationService {
|
||||||
|
static final GlobalKey<NavigatorState> rootNavigatorKey =
|
||||||
|
GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
const AppNavigationService._();
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||||
|
import 'package:spotiflac_android/services/app_navigation_service.dart';
|
||||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||||
import 'package:spotiflac_android/utils/logger.dart';
|
import 'package:spotiflac_android/utils/logger.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -30,6 +36,7 @@ bool _containsHttpStatusCode(String message, String code) {
|
|||||||
Future<bool> openPendingExtensionVerification(
|
Future<bool> openPendingExtensionVerification(
|
||||||
String extensionId, {
|
String extensionId, {
|
||||||
String browserMode = 'external_first',
|
String browserMode = 'external_first',
|
||||||
|
void Function(Uri authUri)? onAuthUri,
|
||||||
}) async {
|
}) async {
|
||||||
final normalizedExtensionId = extensionId.trim();
|
final normalizedExtensionId = extensionId.trim();
|
||||||
if (normalizedExtensionId.isEmpty) return false;
|
if (normalizedExtensionId.isEmpty) return false;
|
||||||
@@ -43,19 +50,9 @@ Future<bool> openPendingExtensionVerification(
|
|||||||
|
|
||||||
final uri = Uri.tryParse(authUrl);
|
final uri = Uri.tryParse(authUrl);
|
||||||
if (uri == null) return false;
|
if (uri == null) return false;
|
||||||
|
onAuthUri?.call(uri);
|
||||||
|
|
||||||
final preferInApp = browserMode.trim().toLowerCase() == 'in_app_first';
|
final launched = await _launchVerificationUrl(uri, browserMode);
|
||||||
final firstMode = preferInApp
|
|
||||||
? LaunchMode.inAppBrowserView
|
|
||||||
: LaunchMode.externalApplication;
|
|
||||||
final fallbackMode = preferInApp
|
|
||||||
? LaunchMode.externalApplication
|
|
||||||
: LaunchMode.inAppBrowserView;
|
|
||||||
|
|
||||||
var launched = await launchUrl(uri, mode: firstMode);
|
|
||||||
if (!launched) {
|
|
||||||
launched = await launchUrl(uri, mode: fallbackMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (launched) {
|
if (launched) {
|
||||||
_log.i('Opened verification challenge for $normalizedExtensionId');
|
_log.i('Opened verification challenge for $normalizedExtensionId');
|
||||||
@@ -63,6 +60,12 @@ Future<bool> openPendingExtensionVerification(
|
|||||||
_log.w(
|
_log.w(
|
||||||
'Could not open verification challenge for $normalizedExtensionId',
|
'Could not open verification challenge for $normalizedExtensionId',
|
||||||
);
|
);
|
||||||
|
return showExtensionVerificationHelpDialog(
|
||||||
|
normalizedExtensionId,
|
||||||
|
uri,
|
||||||
|
browserMode: browserMode,
|
||||||
|
immediateFailure: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return launched;
|
return launched;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -72,3 +75,119 @@ Future<bool> openPendingExtensionVerification(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer? scheduleExtensionVerificationHelpDialog(
|
||||||
|
String extensionId,
|
||||||
|
Uri? authUri, {
|
||||||
|
String browserMode = 'external_first',
|
||||||
|
Duration delay = const Duration(seconds: 20),
|
||||||
|
}) {
|
||||||
|
final normalizedExtensionId = extensionId.trim();
|
||||||
|
if (normalizedExtensionId.isEmpty || authUri == null) return null;
|
||||||
|
|
||||||
|
return Timer(delay, () {
|
||||||
|
unawaited(
|
||||||
|
showExtensionVerificationHelpDialog(
|
||||||
|
normalizedExtensionId,
|
||||||
|
authUri,
|
||||||
|
browserMode: browserMode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showExtensionVerificationHelpDialog(
|
||||||
|
String extensionId,
|
||||||
|
Uri authUri, {
|
||||||
|
String browserMode = 'external_first',
|
||||||
|
bool immediateFailure = false,
|
||||||
|
}) async {
|
||||||
|
final context = AppNavigationService.rootNavigatorKey.currentContext;
|
||||||
|
if (context == null) {
|
||||||
|
_log.w('Cannot show verification help dialog without root context');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final title = immediateFailure
|
||||||
|
? l10n.extensionVerificationHelpTitleManual
|
||||||
|
: l10n.extensionVerificationHelpTitleWaiting;
|
||||||
|
final message = immediateFailure
|
||||||
|
? l10n.extensionVerificationHelpMessageManual
|
||||||
|
: l10n.extensionVerificationHelpMessageWaiting;
|
||||||
|
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
final dialogL10n = dialogContext.l10n;
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(message),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(dialogContext).colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: SelectableText(
|
||||||
|
authUri.toString(),
|
||||||
|
maxLines: 4,
|
||||||
|
minLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: Text(dialogL10n.extensionVerificationClose),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
label: Text(dialogL10n.extensionVerificationCopyLink),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: authUri.toString()));
|
||||||
|
ScaffoldMessenger.maybeOf(dialogContext)?.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(dialogL10n.extensionVerificationLinkCopied),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FilledButton.icon(
|
||||||
|
icon: const Icon(Icons.open_in_browser),
|
||||||
|
label: Text(dialogL10n.extensionVerificationOpenBrowser),
|
||||||
|
onPressed: () {
|
||||||
|
unawaited(_launchVerificationUrl(authUri, browserMode));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _launchVerificationUrl(Uri uri, String browserMode) async {
|
||||||
|
final preferInApp = browserMode.trim().toLowerCase() == 'in_app_first';
|
||||||
|
final firstMode = preferInApp
|
||||||
|
? LaunchMode.inAppBrowserView
|
||||||
|
: LaunchMode.externalApplication;
|
||||||
|
final fallbackMode = preferInApp
|
||||||
|
? LaunchMode.externalApplication
|
||||||
|
: LaunchMode.inAppBrowserView;
|
||||||
|
|
||||||
|
var launched = await launchUrl(uri, mode: firstMode);
|
||||||
|
if (!launched) {
|
||||||
|
launched = await launchUrl(uri, mode: fallbackMode);
|
||||||
|
}
|
||||||
|
return launched;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user