From 47deb9f34e060e8479a9bed33dacde6b5626ec7e Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 27 Aug 2026 13:08:44 +0700 Subject: [PATCH] fix(extensions): resume metadata after verification --- lib/screens/home_tab.dart | 1 + lib/screens/home_tab_widgets.dart | 42 +++++++++++++++++------- lib/utils/extension_auth_launcher.dart | 23 +++++++++++++ test/extension_auth_launcher_test.dart | 45 ++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 12 deletions(-) diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index b8cb8d17..62b2a269 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -26,6 +26,7 @@ import 'package:spotiflac_android/services/cover_download_service.dart'; import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; +import 'package:spotiflac_android/utils/extension_auth_launcher.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index db5ccf42..9e4113c2 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -932,10 +932,16 @@ class _ExtensionAlbumScreenState extends ConsumerState { }); try { - final result = await PlatformBridge.getProviderMetadata( - widget.extensionId, - 'album', - widget.albumId, + final result = await runExtensionOperationWithVerificationRetry( + extensionId: widget.extensionId, + browserMode: ref + .read(settingsProvider) + .extensionVerificationBrowserMode, + operation: () => PlatformBridge.getProviderMetadata( + widget.extensionId, + 'album', + widget.albumId, + ), ); if (!mounted) return; @@ -1106,10 +1112,16 @@ class _ExtensionPlaylistScreenState }); try { - final result = await PlatformBridge.getProviderMetadata( - widget.extensionId, - 'playlist', - widget.playlistId, + final result = await runExtensionOperationWithVerificationRetry( + extensionId: widget.extensionId, + browserMode: ref + .read(settingsProvider) + .extensionVerificationBrowserMode, + operation: () => PlatformBridge.getProviderMetadata( + widget.extensionId, + 'playlist', + widget.playlistId, + ), ); if (!mounted) return; @@ -1251,10 +1263,16 @@ class _ExtensionArtistScreenState extends ConsumerState }); try { - final result = await PlatformBridge.getProviderMetadata( - widget.extensionId, - 'artist', - widget.artistId, + final result = await runExtensionOperationWithVerificationRetry( + extensionId: widget.extensionId, + browserMode: ref + .read(settingsProvider) + .extensionVerificationBrowserMode, + operation: () => PlatformBridge.getProviderMetadata( + widget.extensionId, + 'artist', + widget.artistId, + ), ); if (!mounted) return; diff --git a/lib/utils/extension_auth_launcher.dart b/lib/utils/extension_auth_launcher.dart index bf565ece..6881e784 100644 --- a/lib/utils/extension_auth_launcher.dart +++ b/lib/utils/extension_auth_launcher.dart @@ -25,6 +25,29 @@ bool isExtensionVerificationRequired(Object error) { _containsHttpStatusCode(message, '428'); } +Future runExtensionOperationWithVerificationRetry({ + required String extensionId, + required String browserMode, + required Future Function() operation, + Future Function()? verify, +}) async { + try { + return await operation(); + } catch (error) { + if (!isExtensionVerificationRequired(error)) rethrow; + + final verified = + await (verify ?? + () => openVerificationAndAwaitGrant( + extensionId, + browserMode: browserMode, + ))(); + if (!verified) rethrow; + + return operation(); + } +} + String? extensionIdFromVerificationError( Object error, Iterable knownExtensionIds, diff --git a/test/extension_auth_launcher_test.dart b/test/extension_auth_launcher_test.dart index 6ab632ce..d54fbc7b 100644 --- a/test/extension_auth_launcher_test.dart +++ b/test/extension_auth_launcher_test.dart @@ -24,6 +24,51 @@ void main() { ); }); + test( + 'retries an extension operation after successful verification', + () async { + var operationCalls = 0; + var verificationCalls = 0; + + final result = await runExtensionOperationWithVerificationRetry( + extensionId: 'qobuz-web', + browserMode: 'in_app_first', + operation: () async { + operationCalls++; + if (operationCalls == 1) throw Exception('VERIFY_REQUIRED'); + return 'artist metadata'; + }, + verify: () async { + verificationCalls++; + return true; + }, + ); + + expect(result, 'artist metadata'); + expect(operationCalls, 2); + expect(verificationCalls, 1); + }, + ); + + test('does not retry when extension verification is not completed', () async { + var operationCalls = 0; + + await expectLater( + runExtensionOperationWithVerificationRetry( + extensionId: 'qobuz-web', + browserMode: 'in_app_first', + operation: () async { + operationCalls++; + throw Exception('VERIFY_REQUIRED'); + }, + verify: () async => false, + ), + throwsA(isA()), + ); + + expect(operationCalls, 1); + }); + test('verification wait can be cancelled before foreground resume', () async { final foreground = Completer(); final cancellation = Completer();