From a5b7e5faf0b3923ff9d9571dfb9f69f9b7e9d11c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 10 Feb 2023 17:30:31 +0600 Subject: [PATCH] refactor(authentication): immutable authentication state --- lib/components/desktop_login/login_form.dart | 13 +- lib/components/library/user_albums.dart | 6 +- lib/components/library/user_artists.dart | 6 +- lib/components/library/user_playlists.dart | 6 +- lib/components/player/player_actions.dart | 6 +- lib/components/root/sidebar.dart | 8 +- .../shared/dialogs/prompt_dialog.dart | 5 +- .../shared/fallbacks/anonymous_fallback.dart | 4 +- lib/components/shared/heart_button.dart | 6 +- .../track_table/track_collection_view.dart | 8 +- .../shared/track_table/track_tile.dart | 8 +- lib/pages/artist/artist.dart | 6 +- lib/pages/desktop_login/login_tutorial.dart | 10 +- lib/pages/home/genres.dart | 8 +- lib/pages/mobile_login/mobile_login.dart | 13 +- lib/pages/search/search.dart | 8 +- lib/pages/settings/settings.dart | 16 ++- lib/provider/auth_provider.dart | 124 ---------------- lib/provider/authentication_provider.dart | 133 ++++++++++++++++++ lib/provider/spotify_provider.dart | 8 +- lib/utils/persisted_state_notifier.dart | 9 +- lib/utils/service_utils.dart | 18 --- 22 files changed, 213 insertions(+), 216 deletions(-) delete mode 100644 lib/provider/auth_provider.dart create mode 100644 lib/provider/authentication_provider.dart diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart index e138ef6a..3ee493a6 100644 --- a/lib/components/desktop_login/login_form.dart +++ b/lib/components/desktop_login/login_form.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; -import 'package:spotube/provider/auth_provider.dart'; -import 'package:spotube/utils/service_utils.dart'; +import 'package:spotube/provider/authentication_provider.dart'; class TokenLoginForm extends HookConsumerWidget { final void Function()? onDone; @@ -14,7 +13,8 @@ class TokenLoginForm extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - Auth authState = ref.watch(authProvider); + final authenticationNotifier = + ref.watch(AuthenticationNotifier.provider.notifier); final directCodeController = useTextEditingController(); final keyCodeController = useTextEditingController(); final mounted = useIsMounted(); @@ -53,12 +53,9 @@ class TokenLoginForm extends HookConsumerWidget { } final cookieHeader = "sp_dc=${directCodeController.text}; sp_key=${keyCodeController.text}"; - final body = await ServiceUtils.getAccessToken(cookieHeader); - authState.setAuthState( - accessToken: body.accessToken, - authCookie: cookieHeader, - expiration: body.expiration, + authenticationNotifier.setCredentials( + await AuthenticationCredentials.fromCookie(cookieHeader), ); if (mounted()) { onDone?.call(); diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 4d914ed2..db58ddd7 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -23,7 +23,7 @@ class UserAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final albumsQuery = useQuery( job: Queries.album.ofMine, externalData: ref.watch(spotifyProvider), @@ -55,7 +55,7 @@ class UserAlbums extends HookConsumerWidget { []; }, [albumsQuery.data, searchText.value]); - if (auth.isAnonymous) { + if (auth == null) { return const AnonymousFallback(); } if (albumsQuery.isLoading || !albumsQuery.hasData) { diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 0b6ab408..8c8d95d9 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -10,7 +10,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/artist/artist_card.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:tuple/tuple.dart'; @@ -20,7 +20,7 @@ class UserArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final artistQuery = useInfiniteQuery( job: Queries.artist.followedByMe, @@ -51,7 +51,7 @@ class UserArtists extends HookConsumerWidget { .toList(); }, [artistQuery.pages, searchText.value]); - if (auth.isAnonymous) { + if (auth == null) { return const AnonymousFallback(); } diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index cf01792d..2ff691cd 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:tuple/tuple.dart'; @@ -33,7 +33,7 @@ class UserPlaylists extends HookConsumerWidget { final viewType = MediaQuery.of(context).size.width < 480 ? PlaybuttonCardViewType.list : PlaybuttonCardViewType.square; - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final playlistsQuery = useQuery( job: Queries.playlist.ofMine, @@ -76,7 +76,7 @@ class UserPlaylists extends HookConsumerWidget { [playlistsQuery.data, searchText.value], ); - if (auth.isAnonymous) { + if (auth == null) { return const AnonymousFallback(); } diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index cf0de7ac..10be9f29 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -10,7 +10,7 @@ import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -36,7 +36,7 @@ class PlayerActions extends HookConsumerWidget { final isInQueue = downloader.inQueue .any((element) => element.id == playlist?.activeTrack.id); final localTracks = [] /* ref.watch(localTracksProvider).value */; - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final isDownloaded = useMemoized(() { return localTracks.any( @@ -124,7 +124,7 @@ class PlayerActions extends HookConsumerWidget { ? () => downloader.addToQueue(playlist!.activeTrack) : null, ), - if (playlist?.activeTrack != null && !isLocalTrack && auth.isLoggedIn) + if (playlist?.activeTrack != null && !isLocalTrack && auth != null) TrackHeartButton(track: playlist!.activeTrack), ...(extraActions ?? []) ], diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 3794b40e..4f5dff8c 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -10,7 +10,7 @@ import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; @@ -210,10 +210,10 @@ class SidebarFooter extends HookConsumerWidget { // TODO: Remove below code after fl-query ^0.4.0 /// Temporary fix before fl-query 0.4.0 - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); useEffect(() { - if (auth.isLoggedIn && me.hasError) { + if (auth != null && me.hasError) { me.setExternalData(spotify); me.refetch(); } @@ -227,7 +227,7 @@ class SidebarFooter extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (auth.isLoggedIn && data == null) + if (auth != null && data == null) const Center( child: PlatformCircularProgressIndicator(), ) diff --git a/lib/components/shared/dialogs/prompt_dialog.dart b/lib/components/shared/dialogs/prompt_dialog.dart index 1bc4af92..0cc86e67 100644 --- a/lib/components/shared/dialogs/prompt_dialog.dart +++ b/lib/components/shared/dialogs/prompt_dialog.dart @@ -19,7 +19,8 @@ Future showPromptDialog({ primaryActions: [ if (platform == TargetPlatform.iOS) CupertinoDialogAction( - isDefaultAction: true, + isDefaultAction: false, + isDestructiveAction: true, child: PlatformText(okText), onPressed: () => Navigator.of(context).pop(true), ) @@ -32,7 +33,7 @@ Future showPromptDialog({ secondaryActions: [ if (platform == TargetPlatform.iOS) CupertinoDialogAction( - isDefaultAction: false, + isDefaultAction: true, child: PlatformText(cancelText), onPressed: () => Navigator.of(context).pop(false), ) diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/shared/fallbacks/anonymous_fallback.dart index 979168d9..aa72d462 100644 --- a/lib/components/shared/fallbacks/anonymous_fallback.dart +++ b/lib/components/shared/fallbacks/anonymous_fallback.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/service_utils.dart'; class AnonymousFallback extends ConsumerWidget { @@ -13,7 +13,7 @@ class AnonymousFallback extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final isLoggedIn = ref.watch(authProvider.select((s) => s.isLoggedIn)); + final isLoggedIn = ref.watch(AuthenticationNotifier.provider) != null; if (isLoggedIn && child != null) return child!; return Center( diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 72973077..49428c27 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -7,7 +7,7 @@ import 'package:platform_ui/platform_ui.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/hooks/use_palette_color.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -32,9 +32,9 @@ class HeartButton extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); - if (!auth.isLoggedIn) return Container(); + if (auth == null) return Container(); return PlatformIconButton( tooltip: tooltip, diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index d242f501..3be7773a 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -12,12 +12,12 @@ import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; -import 'package:spotube/provider/auth_provider.dart'; import 'package:spotube/hooks/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:tuple/tuple.dart'; @@ -64,7 +64,7 @@ class TrackCollectionView extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final color = usePaletteGenerator( context, titleImage, @@ -79,7 +79,7 @@ class TrackCollectionView extends HookConsumerWidget { ), onPressed: onShare, ), - if (heartBtn != null && auth.isLoggedIn) heartBtn!, + if (heartBtn != null && auth != null) heartBtn!, PlatformIconButton( tooltip: "Shuffle", icon: Icon( @@ -194,7 +194,7 @@ class TrackCollectionView extends HookConsumerWidget { child: searchbar, ); }); - Overlay.of(context)?.insert(entry!); + Overlay.of(context).insert(entry!); } }); return () => entry?.remove(); diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 2977104c..615d453b 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -16,7 +16,7 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; @@ -72,7 +72,7 @@ class TrackTile extends HookConsumerWidget { ), ), ); - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final spotify = ref.watch(spotifyProvider); final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier); @@ -362,13 +362,13 @@ class TrackTile extends HookConsumerWidget { toggler.item2.mutate(Tuple2(spotify, toggler.item1)); }, ), - if (auth.isLoggedIn) + if (auth != null) Action( icon: const Icon(SpotubeIcons.playlistAdd), text: const PlatformText("Add To playlist"), onPressed: actionAddToPlaylist, ), - if (userPlaylist && auth.isLoggedIn) + if (userPlaylist && auth != null) Action( icon: (removeTrack.isLoading || !removeTrack.hasData) && removingTrack.value == track.value.uri diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 4b0f68f2..78894945 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -16,7 +16,7 @@ import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; @@ -56,7 +56,7 @@ class ArtistPage extends HookConsumerWidget { final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlist = ref.watch(PlaylistQueueNotifier.provider); - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); return SafeArea( child: PlatformScaffold( @@ -163,7 +163,7 @@ class ArtistPage extends HookConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - if (auth.isLoggedIn) + if (auth != null) HookBuilder( builder: (context) { final isFollowingQuery = useQuery( diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index 2ec2249f..8f211013 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/desktop_login/login_form.dart'; import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/service_utils.dart'; class LoginTutorial extends ConsumerWidget { @@ -15,7 +15,9 @@ class LoginTutorial extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authProvider); + ref.watch(AuthenticationNotifier.provider); + final authenticationNotifier = + ref.watch(AuthenticationNotifier.provider.notifier); final key = GlobalKey>(); final pageDecoration = PageDecoration( @@ -51,7 +53,7 @@ class LoginTutorial extends ConsumerWidget { ), showBackButton: true, overrideDone: PlatformFilledButton( - onPressed: auth.isLoggedIn + onPressed: authenticationNotifier.isLoggedIn ? () { ServiceUtils.navigate(context, "/"); } @@ -96,7 +98,7 @@ class LoginTutorial extends ConsumerWidget { textAlign: TextAlign.left, ), ), - if (auth.isLoggedIn) + if (authenticationNotifier.isLoggedIn) PageViewModel( decoration: pageDecoration.copyWith( bodyAlignment: Alignment.center, diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 45d649e7..faeec721 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/genre/category_card.dart'; import 'package:spotube/components/shared/compact_search.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/waypoint.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; @@ -36,11 +36,11 @@ class GenrePage extends HookConsumerWidget { final isMounted = useIsMounted(); - /// Temporary fix before fl-query 0.4.0 - final auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); + /// Temporary fix before fl-query 0.4.0 useEffect(() { - if (auth.isLoggedIn && categoriesQuery.hasError) { + if (auth != null && categoriesQuery.hasError) { categoriesQuery.setExternalData({ "spotify": spotify, "recommendationMarket": recommendationMarket, diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index d50894f9..82edaa78 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -4,9 +4,8 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/platform.dart'; -import 'package:spotube/utils/service_utils.dart'; class WebViewLogin extends HookConsumerWidget { const WebViewLogin({Key? key}) : super(key: key); @@ -14,7 +13,8 @@ class WebViewLogin extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final mounted = useIsMounted(); - final auth = ref.watch(authProvider); + final authenticationNotifier = + ref.watch(AuthenticationNotifier.provider.notifier); if (kIsDesktop) { const Scaffold( @@ -62,11 +62,8 @@ class WebViewLogin extends HookConsumerWidget { return previousValue; }); - final body = await ServiceUtils.getAccessToken(cookieHeader); - auth.setAuthState( - accessToken: body.accessToken, - authCookie: cookieHeader, - expiration: body.expiration, + authenticationNotifier.setCredentials( + await AuthenticationCredentials.fromCookie(cookieHeader), ); if (mounted()) { // ignore: use_build_context_synchronously diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index b2d38c08..1d5de6be 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -16,7 +16,7 @@ import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -34,7 +34,9 @@ class SearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authProvider); + ref.watch(AuthenticationNotifier.provider); + final authenticationNotifier = + ref.watch(AuthenticationNotifier.provider.notifier); final spotify = ref.watch(spotifyProvider); final albumController = useScrollController(); final playlistController = useScrollController(); @@ -83,7 +85,7 @@ class SearchPage extends HookConsumerWidget { return SafeArea( child: PlatformScaffold( appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null, - body: auth.isAnonymous + body: !authenticationNotifier.isLoggedIn ? const AnonymousFallback() : Column( children: [ diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 8e6a0889..0b609e29 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/main.dart'; import 'package:spotube/collections/spotify_markets.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -21,7 +21,7 @@ class SettingsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final UserPreferences preferences = ref.watch(userPreferencesProvider); - final Auth auth = ref.watch(authProvider); + final auth = ref.watch(AuthenticationNotifier.provider); final pickColorScheme = useCallback((ColorSchemeType schemeType) { return () => showPlatformAlertDialog(context, builder: (context) { @@ -59,7 +59,7 @@ class SettingsPage extends HookConsumerWidget { .headline ?.copyWith(fontWeight: FontWeight.bold), ), - if (auth.isAnonymous) + if (auth == null) AdaptiveListTile( leading: Icon( SpotubeIcons.login, @@ -93,10 +93,9 @@ class SettingsPage extends HookConsumerWidget { child: PlatformText( "Connect with Spotify".toUpperCase()), ), - ), - if (auth.isLoggedIn) + ) + else Builder(builder: (context) { - Auth auth = ref.watch(authProvider); return PlatformListTile( leading: const Icon(SpotubeIcons.logout), title: SizedBox( @@ -119,7 +118,10 @@ class SettingsPage extends HookConsumerWidget { MaterialStateProperty.all(Colors.white), ), onPressed: () async { - auth.logout(); + ref + .read( + AuthenticationNotifier.provider.notifier) + .logout(); GoRouter.of(context).pop(); }, child: const PlatformText("Logout"), diff --git a/lib/provider/auth_provider.dart b/lib/provider/auth_provider.dart deleted file mode 100644 index 47b73d5a..00000000 --- a/lib/provider/auth_provider.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/utils/persisted_change_notifier.dart'; -import 'package:spotube/utils/platform.dart'; -import 'package:spotube/utils/service_utils.dart'; - -class Auth extends PersistedChangeNotifier { - String? _accessToken; - DateTime? _expiration; - String? _authCookie; - - Timer? _refresher; - - Auth() : super() { - _refresher = _createRefresher(); - } - - String? get accessToken => _accessToken; - DateTime? get expiration => _expiration; - String? get authCookie => _authCookie; - - bool get isAnonymous => accessToken == null && authCookie == null; - - bool get isLoggedIn => !isAnonymous && _expiration != null; - bool get isExpired => - _expiration != null && _expiration!.isBefore(DateTime.now()); - - Duration get expiresIn => - _expiration?.difference(DateTime.now()) ?? Duration.zero; - - Future refresh() async { - final data = await ServiceUtils.getAccessToken(authCookie!); - _accessToken = data.accessToken; - _expiration = data.expiration; - _restartRefresher(); - notifyListeners(); - } - - Timer? _createRefresher() { - if (expiration == null || authCookie == null) { - return null; - } - if (isExpired) { - refresh(); - } - _refresher?.cancel(); - return Timer(expiresIn - const Duration(minutes: 5), refresh); - } - - void _restartRefresher() { - _refresher = _createRefresher(); - } - - void setAuthState({ - bool safe = true, - String? accessToken, - DateTime? expiration, - String? authCookie, - }) { - if (safe) { - if (accessToken != null) _accessToken = accessToken; - if (expiration != null) { - _expiration = expiration; - _restartRefresher(); - } - if (authCookie != null) _authCookie = authCookie; - } else { - _accessToken = accessToken; - _expiration = expiration; - _authCookie = authCookie; - - _restartRefresher(); - } - notifyListeners(); - updatePersistence(); - } - - void logout() { - _accessToken = null; - _expiration = null; - _authCookie = null; - _refresher?.cancel(); - _refresher = null; - if (kIsMobile) { - WebStorageManager.instance().android.deleteAllData(); - CookieManager.instance().deleteAllCookies(); - } - notifyListeners(); - updatePersistence(clearNullEntries: true); - } - - @override - String toString() { - return "Auth(accessToken: $accessToken, expiration: $expiration, isLoggedIn: $isLoggedIn, isAnonymous: $isAnonymous, authCookie: $authCookie)"; - } - - @override - FutureOr loadFromLocal(Map map) async { - _accessToken = map["accessToken"]; - _expiration = map["expiration"] != null - ? DateTime.tryParse(map["expiration"]) - : _expiration; - _authCookie = map["authCookie"]; - if (isExpired) { - final data = await ServiceUtils.getAccessToken(authCookie!); - _accessToken = data.accessToken; - _expiration = data.expiration; - } - _restartRefresher(); - } - - @override - FutureOr> toMap() { - return { - "accessToken": _accessToken, - "expiration": _expiration.toString(), - "authCookie": _authCookie, - }; - } -} - -final authProvider = ChangeNotifierProvider((ref) => Auth()); diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication_provider.dart new file mode 100644 index 00000000..057ddda2 --- /dev/null +++ b/lib/provider/authentication_provider.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/utils/platform.dart'; + +class AuthenticationCredentials { + String cookie; + String accessToken; + DateTime expiration; + + bool get isExpired => DateTime.now().isAfter(expiration); + + AuthenticationCredentials({ + required this.cookie, + required this.accessToken, + required this.expiration, + }); + + static Future fromCookie(String cookie) async { + final Map body = await get( + Uri.parse( + "https://open.spotify.com/get_access_token?reason=transport&productType=web_player", + ), + headers: { + "Cookie": cookie, + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" + }, + ).then((res) => jsonDecode(res.body)); + + return AuthenticationCredentials( + cookie: cookie, + accessToken: body['accessToken'], + expiration: DateTime.fromMillisecondsSinceEpoch( + body['accessTokenExpirationTimestampMs'], + ), + ); + } + + factory AuthenticationCredentials.fromJson(Map json) { + return AuthenticationCredentials( + cookie: json['cookie'] as String, + accessToken: json['accessToken'] as String, + expiration: DateTime.parse(json['expiration'] as String), + ); + } + + Map toJson() { + return { + 'cookie': cookie, + 'accessToken': accessToken, + 'expiration': expiration.toIso8601String(), + }; + } + + AuthenticationCredentials copyWith({ + String? cookie, + String? accessToken, + DateTime? expiration, + }) { + return AuthenticationCredentials( + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } +} + +class AuthenticationNotifier + extends PersistedStateNotifier { + static final provider = + StateNotifierProvider( + (ref) => AuthenticationNotifier(), + ); + + bool get isLoggedIn => state != null; + + AuthenticationNotifier() : super(null, "authentication"); + + Timer? _refreshTimer; + + @override + FutureOr onInit() async { + super.onInit(); + if (isLoggedIn && state!.isExpired) { + await refreshCredentials(); + } + + addListener((state) { + _refreshTimer?.cancel(); + if (isLoggedIn && !state!.isExpired) { + _refreshTimer = Timer( + state.expiration.difference(DateTime.now()), + () => refreshCredentials(), + ); + } + }); + } + + void setCredentials(AuthenticationCredentials credentials) { + state = credentials; + } + + Future logout() async { + state = null; + if (kIsMobile) { + WebStorageManager.instance().android.deleteAllData(); + CookieManager.instance().deleteAllCookies(); + } + } + + Future refreshCredentials() async { + if (!isLoggedIn) { + return; + } + + state = await AuthenticationCredentials.fromCookie(state!.cookie); + } + + @override + FutureOr fromJson(Map json) { + return AuthenticationCredentials.fromJson(json); + } + + @override + Map toJson() { + return state?.toJson() ?? {}; + } +} diff --git a/lib/provider/spotify_provider.dart b/lib/provider/spotify_provider.dart index f25893de..cd557c39 100644 --- a/lib/provider/spotify_provider.dart +++ b/lib/provider/spotify_provider.dart @@ -1,14 +1,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/generated_secrets.dart'; -import 'package:spotube/provider/auth_provider.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/primitive_utils.dart'; final spotifyProvider = Provider((ref) { - Auth authState = ref.watch(authProvider); + final authState = ref.watch(AuthenticationNotifier.provider); final anonCred = PrimitiveUtils.getRandomElement(spotifySecrets); - if (authState.isAnonymous) { + if (authState == null) { return SpotifyApi( SpotifyApiCredentials( anonCred["clientId"], @@ -17,5 +17,5 @@ final spotifyProvider = Provider((ref) { ); } - return SpotifyApi.withAccessToken(authState.accessToken!); + return SpotifyApi.withAccessToken(authState.accessToken); }); diff --git a/lib/utils/persisted_state_notifier.dart b/lib/utils/persisted_state_notifier.dart index 99f3b9aa..78219051 100644 --- a/lib/utils/persisted_state_notifier.dart +++ b/lib/utils/persisted_state_notifier.dart @@ -6,8 +6,13 @@ import 'package:hive/hive.dart'; abstract class PersistedStateNotifier extends StateNotifier { final String cacheKey; - PersistedStateNotifier(super.state, this.cacheKey) { - _load(); + FutureOr onInit() {} + + PersistedStateNotifier( + super.state, + this.cacheKey, + ) { + _load().then((_) => onInit()); } Future _load() async { diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 3bdef94d..9264f535 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -8,7 +8,6 @@ import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/models/logger.dart'; import 'package:http/http.dart' as http; import 'package:spotube/models/lyrics.dart'; -import 'package:spotube/models/spotify_spotube_credentials.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/utils/primitive_utils.dart'; @@ -246,23 +245,6 @@ abstract class ServiceUtils { return subtitle; } - static Future getAccessToken( - String cookieHeader) async { - final res = await http.get( - Uri.parse( - "https://open.spotify.com/get_access_token?reason=transport&productType=web_player", - ), - headers: { - "Cookie": cookieHeader, - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" - }, - ); - return SpotifySpotubeCredentials.fromJson( - jsonDecode(res.body), - ); - } - static void navigate(BuildContext context, String location, {Object? extra}) { GoRouter.of(context).push(location, extra: extra); }