mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
fix(extensions): serialize verification callbacks
This commit is contained in:
@@ -1409,9 +1409,37 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
static final Map<String, Future<bool>> _extensionSessionGrantCompletions =
|
||||
<String, Future<bool>>{};
|
||||
|
||||
static Future<bool> completeExtensionSessionGrant(
|
||||
String extensionId,
|
||||
String grant,
|
||||
) {
|
||||
final normalizedExtensionId = extensionId.trim();
|
||||
final key = normalizedExtensionId.toLowerCase();
|
||||
final activeCompletion = _extensionSessionGrantCompletions[key];
|
||||
if (activeCompletion != null) {
|
||||
_log.d(
|
||||
'Joining active completeExtensionSessionGrant: $normalizedExtensionId',
|
||||
);
|
||||
return activeCompletion;
|
||||
}
|
||||
|
||||
late final Future<bool> completion;
|
||||
completion = _completeExtensionSessionGrant(normalizedExtensionId, grant)
|
||||
.whenComplete(() {
|
||||
if (identical(_extensionSessionGrantCompletions[key], completion)) {
|
||||
_extensionSessionGrantCompletions.remove(key);
|
||||
}
|
||||
});
|
||||
_extensionSessionGrantCompletions[key] = completion;
|
||||
return completion;
|
||||
}
|
||||
|
||||
static Future<bool> _completeExtensionSessionGrant(
|
||||
String extensionId,
|
||||
String grant,
|
||||
) async {
|
||||
_log.d('completeExtensionSessionGrant: $extensionId');
|
||||
final result = await _channel.invokeMethod(
|
||||
|
||||
@@ -177,6 +177,7 @@ Future<bool> showExtensionVerificationHelpDialog(
|
||||
: l10n.extensionVerificationHelpMessageWaiting;
|
||||
final normalizedExtensionId = extensionId.trim();
|
||||
BuildContext? activeDialogContext;
|
||||
var clipboardGrantInFlight = false;
|
||||
late final StreamSubscription<ExtensionSessionGrantEvent> grantSub;
|
||||
grantSub = PlatformBridge.extensionSessionGrantEvents()
|
||||
.where(
|
||||
@@ -200,69 +201,90 @@ Future<bool> showExtensionVerificationHelpDialog(
|
||||
builder: (dialogContext) {
|
||||
activeDialogContext = 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,
|
||||
return StatefulBuilder(
|
||||
builder: (dialogContext, setDialogState) => 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: clipboardGrantInFlight
|
||||
? null
|
||||
: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(dialogL10n.extensionVerificationClose),
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
label: Text(dialogL10n.extensionVerificationCopyLink),
|
||||
onPressed: clipboardGrantInFlight
|
||||
? null
|
||||
: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: authUri.toString()),
|
||||
);
|
||||
ScaffoldMessenger.maybeOf(dialogContext)?.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogL10n.extensionVerificationLinkCopied,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: clipboardGrantInFlight
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.content_paste),
|
||||
label: const Text('Paste callback'),
|
||||
onPressed: clipboardGrantInFlight
|
||||
? null
|
||||
: () async {
|
||||
setDialogState(() => clipboardGrantInFlight = true);
|
||||
await _completeSessionGrantFromClipboard(
|
||||
dialogContext,
|
||||
extensionId,
|
||||
);
|
||||
if (dialogContext.mounted) {
|
||||
setDialogState(() => clipboardGrantInFlight = false);
|
||||
}
|
||||
},
|
||||
),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
label: Text(dialogL10n.extensionVerificationOpenBrowser),
|
||||
onPressed: clipboardGrantInFlight
|
||||
? null
|
||||
: () {
|
||||
unawaited(_launchVerificationUrl(authUri, browserMode));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.content_paste),
|
||||
label: const Text('Paste callback'),
|
||||
onPressed: () {
|
||||
unawaited(
|
||||
_completeSessionGrantFromClipboard(
|
||||
dialogContext,
|
||||
extensionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
label: Text(dialogL10n.extensionVerificationOpenBrowser),
|
||||
onPressed: () {
|
||||
unawaited(_launchVerificationUrl(authUri, browserMode));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -304,9 +326,6 @@ Future<void> _completeSessionGrantFromClipboard(
|
||||
),
|
||||
),
|
||||
);
|
||||
if (success) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
messenger?.showSnackBar(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const backendChannel = MethodChannel('com.zarz.spotiflac/backend');
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(backendChannel, null);
|
||||
});
|
||||
|
||||
test('duplicate session grant completions share one native call', () async {
|
||||
final nativeResult = Completer<bool>();
|
||||
var callCount = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(backendChannel, (call) async {
|
||||
if (call.method != 'completeExtensionSessionGrant') return null;
|
||||
callCount++;
|
||||
return nativeResult.future;
|
||||
});
|
||||
|
||||
final events = <ExtensionSessionGrantEvent>[];
|
||||
final subscription = PlatformBridge.extensionSessionGrantEvents().listen(
|
||||
events.add,
|
||||
);
|
||||
addTearDown(subscription.cancel);
|
||||
|
||||
final first = PlatformBridge.completeExtensionSessionGrant(
|
||||
' qobuz-web ',
|
||||
'grant-value',
|
||||
);
|
||||
final second = PlatformBridge.completeExtensionSessionGrant(
|
||||
'QOBUZ-WEB',
|
||||
'grant-value',
|
||||
);
|
||||
|
||||
expect(identical(first, second), isTrue);
|
||||
expect(callCount, 1);
|
||||
|
||||
nativeResult.complete(true);
|
||||
expect(await Future.wait([first, second]), [isTrue, isTrue]);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(callCount, 1);
|
||||
expect(events, hasLength(1));
|
||||
expect(events.single.extensionId, 'qobuz-web');
|
||||
expect(events.single.success, isTrue);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user