refactor(authentication): immutable authentication state

This commit is contained in:
Kingkor Roy Tirtho 2023-02-10 17:30:31 +06:00
parent 0751f5e317
commit a5b7e5faf0
22 changed files with 213 additions and 216 deletions

View File

@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.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 TokenLoginForm extends HookConsumerWidget { class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone; final void Function()? onDone;
@ -14,7 +13,8 @@ class TokenLoginForm extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Auth authState = ref.watch(authProvider); final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier);
final directCodeController = useTextEditingController(); final directCodeController = useTextEditingController();
final keyCodeController = useTextEditingController(); final keyCodeController = useTextEditingController();
final mounted = useIsMounted(); final mounted = useIsMounted();
@ -53,12 +53,9 @@ class TokenLoginForm extends HookConsumerWidget {
} }
final cookieHeader = final cookieHeader =
"sp_dc=${directCodeController.text}; sp_key=${keyCodeController.text}"; "sp_dc=${directCodeController.text}; sp_key=${keyCodeController.text}";
final body = await ServiceUtils.getAccessToken(cookieHeader);
authState.setAuthState( authenticationNotifier.setCredentials(
accessToken: body.accessToken, await AuthenticationCredentials.fromCookie(cookieHeader),
authCookie: cookieHeader,
expiration: body.expiration,
); );
if (mounted()) { if (mounted()) {
onDone?.call(); onDone?.call();

View File

@ -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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/hooks/use_breakpoint_value.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/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -23,7 +23,7 @@ class UserAlbums extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
final albumsQuery = useQuery( final albumsQuery = useQuery(
job: Queries.album.ofMine, job: Queries.album.ofMine,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),
@ -55,7 +55,7 @@ class UserAlbums extends HookConsumerWidget {
[]; [];
}, [albumsQuery.data, searchText.value]); }, [albumsQuery.data, searchText.value]);
if (auth.isAnonymous) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }
if (albumsQuery.isLoading || !albumsQuery.hasData) { if (albumsQuery.isLoading || !albumsQuery.hasData) {

View File

@ -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/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/artist/artist_card.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/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -20,7 +20,7 @@ class UserArtists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
final artistQuery = useInfiniteQuery( final artistQuery = useInfiniteQuery(
job: Queries.artist.followedByMe, job: Queries.artist.followedByMe,
@ -51,7 +51,7 @@ class UserArtists extends HookConsumerWidget {
.toList(); .toList();
}, [artistQuery.pages, searchText.value]); }, [artistQuery.pages, searchText.value]);
if (auth.isAnonymous) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }

View File

@ -14,7 +14,7 @@ import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/hooks/use_breakpoints.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/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -33,7 +33,7 @@ class UserPlaylists extends HookConsumerWidget {
final viewType = MediaQuery.of(context).size.width < 480 final viewType = MediaQuery.of(context).size.width < 480
? PlaybuttonCardViewType.list ? PlaybuttonCardViewType.list
: PlaybuttonCardViewType.square; : PlaybuttonCardViewType.square;
final auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
final playlistsQuery = useQuery( final playlistsQuery = useQuery(
job: Queries.playlist.ofMine, job: Queries.playlist.ofMine,
@ -76,7 +76,7 @@ class UserPlaylists extends HookConsumerWidget {
[playlistsQuery.data, searchText.value], [playlistsQuery.data, searchText.value],
); );
if (auth.isAnonymous) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }

View File

@ -10,7 +10,7 @@ import 'package:spotube/components/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.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/downloader_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -36,7 +36,7 @@ class PlayerActions extends HookConsumerWidget {
final isInQueue = downloader.inQueue final isInQueue = downloader.inQueue
.any((element) => element.id == playlist?.activeTrack.id); .any((element) => element.id == playlist?.activeTrack.id);
final localTracks = [] /* ref.watch(localTracksProvider).value */; final localTracks = [] /* ref.watch(localTracksProvider).value */;
final auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
final isDownloaded = useMemoized(() { final isDownloaded = useMemoized(() {
return localTracks.any( return localTracks.any(
@ -124,7 +124,7 @@ class PlayerActions extends HookConsumerWidget {
? () => downloader.addToQueue(playlist!.activeTrack) ? () => downloader.addToQueue(playlist!.activeTrack)
: null, : null,
), ),
if (playlist?.activeTrack != null && !isLocalTrack && auth.isLoggedIn) if (playlist?.activeTrack != null && !isLocalTrack && auth != null)
TrackHeartButton(track: playlist!.activeTrack), TrackHeartButton(track: playlist!.activeTrack),
...(extraActions ?? []) ...(extraActions ?? [])
], ],

View File

@ -10,7 +10,7 @@ import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoints.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/downloader_provider.dart';
import 'package:spotube/provider/spotify_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 // TODO: Remove below code after fl-query ^0.4.0
/// Temporary fix before 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(() { useEffect(() {
if (auth.isLoggedIn && me.hasError) { if (auth != null && me.hasError) {
me.setExternalData(spotify); me.setExternalData(spotify);
me.refetch(); me.refetch();
} }
@ -227,7 +227,7 @@ class SidebarFooter extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (auth.isLoggedIn && data == null) if (auth != null && data == null)
const Center( const Center(
child: PlatformCircularProgressIndicator(), child: PlatformCircularProgressIndicator(),
) )

View File

@ -19,7 +19,8 @@ Future<bool> showPromptDialog({
primaryActions: [ primaryActions: [
if (platform == TargetPlatform.iOS) if (platform == TargetPlatform.iOS)
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: false,
isDestructiveAction: true,
child: PlatformText(okText), child: PlatformText(okText),
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
) )
@ -32,7 +33,7 @@ Future<bool> showPromptDialog({
secondaryActions: [ secondaryActions: [
if (platform == TargetPlatform.iOS) if (platform == TargetPlatform.iOS)
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: false, isDefaultAction: true,
child: PlatformText(cancelText), child: PlatformText(cancelText),
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
) )

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:platform_ui/platform_ui.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'; import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget { class AnonymousFallback extends ConsumerWidget {
@ -13,7 +13,7 @@ class AnonymousFallback extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { 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!; if (isLoggedIn && child != null) return child!;
return Center( return Center(

View File

@ -7,7 +7,7 @@ import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/hooks/use_palette_color.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/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -32,9 +32,9 @@ class HeartButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { 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( return PlatformIconButton(
tooltip: tooltip, tooltip: tooltip,

View File

@ -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/page_window_title_bar.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/track_table/tracks_table_view.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_custom_status_bar_color.dart';
import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/hooks/use_palette_color.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -64,7 +64,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
final color = usePaletteGenerator( final color = usePaletteGenerator(
context, context,
titleImage, titleImage,
@ -79,7 +79,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
), ),
onPressed: onShare, onPressed: onShare,
), ),
if (heartBtn != null && auth.isLoggedIn) heartBtn!, if (heartBtn != null && auth != null) heartBtn!,
PlatformIconButton( PlatformIconButton(
tooltip: "Shuffle", tooltip: "Shuffle",
icon: Icon( icon: Icon(
@ -194,7 +194,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
child: searchbar, child: searchbar,
); );
}); });
Overlay.of(context)?.insert(entry!); Overlay.of(context).insert(entry!);
} }
}); });
return () => entry?.remove(); return () => entry?.remove();

View File

@ -16,7 +16,7 @@ import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/sidebar.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/models/logger.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/blacklist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_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 spotify = ref.watch(spotifyProvider);
final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier);
@ -362,13 +362,13 @@ class TrackTile extends HookConsumerWidget {
toggler.item2.mutate(Tuple2(spotify, toggler.item1)); toggler.item2.mutate(Tuple2(spotify, toggler.item1));
}, },
), ),
if (auth.isLoggedIn) if (auth != null)
Action( Action(
icon: const Icon(SpotubeIcons.playlistAdd), icon: const Icon(SpotubeIcons.playlistAdd),
text: const PlatformText("Add To playlist"), text: const PlatformText("Add To playlist"),
onPressed: actionAddToPlaylist, onPressed: actionAddToPlaylist,
), ),
if (userPlaylist && auth.isLoggedIn) if (userPlaylist && auth != null)
Action( Action(
icon: (removeTrack.isLoading || !removeTrack.hasData) && icon: (removeTrack.isLoading || !removeTrack.hasData) &&
removingTrack.value == track.value.uri removingTrack.value == track.value.uri

View File

@ -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_breakpoint_value.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/models/logger.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/blacklist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_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 playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final playlist = ref.watch(PlaylistQueueNotifier.provider); final playlist = ref.watch(PlaylistQueueNotifier.provider);
final auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
return SafeArea( return SafeArea(
child: PlatformScaffold( child: PlatformScaffold(
@ -163,7 +163,7 @@ class ArtistPage extends HookConsumerWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (auth.isLoggedIn) if (auth != null)
HookBuilder( HookBuilder(
builder: (context) { builder: (context) {
final isFollowingQuery = useQuery( final isFollowingQuery = useQuery(

View File

@ -7,7 +7,7 @@ import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/desktop_login/login_form.dart'; import 'package:spotube/components/desktop_login/login_form.dart';
import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/links/hyper_link.dart';
import 'package:spotube/components/shared/page_window_title_bar.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'; import 'package:spotube/utils/service_utils.dart';
class LoginTutorial extends ConsumerWidget { class LoginTutorial extends ConsumerWidget {
@ -15,7 +15,9 @@ class LoginTutorial extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider); ref.watch(AuthenticationNotifier.provider);
final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier);
final key = GlobalKey<State<IntroductionScreen>>(); final key = GlobalKey<State<IntroductionScreen>>();
final pageDecoration = PageDecoration( final pageDecoration = PageDecoration(
@ -51,7 +53,7 @@ class LoginTutorial extends ConsumerWidget {
), ),
showBackButton: true, showBackButton: true,
overrideDone: PlatformFilledButton( overrideDone: PlatformFilledButton(
onPressed: auth.isLoggedIn onPressed: authenticationNotifier.isLoggedIn
? () { ? () {
ServiceUtils.navigate(context, "/"); ServiceUtils.navigate(context, "/");
} }
@ -96,7 +98,7 @@ class LoginTutorial extends ConsumerWidget {
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
), ),
if (auth.isLoggedIn) if (authenticationNotifier.isLoggedIn)
PageViewModel( PageViewModel(
decoration: pageDecoration.copyWith( decoration: pageDecoration.copyWith(
bodyAlignment: Alignment.center, bodyAlignment: Alignment.center,

View File

@ -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/compact_search.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/waypoint.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/spotify_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
@ -36,11 +36,11 @@ class GenrePage extends HookConsumerWidget {
final isMounted = useIsMounted(); final isMounted = useIsMounted();
/// Temporary fix before fl-query 0.4.0 final auth = ref.watch(AuthenticationNotifier.provider);
final auth = ref.watch(authProvider);
/// Temporary fix before fl-query 0.4.0
useEffect(() { useEffect(() {
if (auth.isLoggedIn && categoriesQuery.hasError) { if (auth != null && categoriesQuery.hasError) {
categoriesQuery.setExternalData({ categoriesQuery.setExternalData({
"spotify": spotify, "spotify": spotify,
"recommendationMarket": recommendationMarket, "recommendationMarket": recommendationMarket,

View File

@ -4,9 +4,8 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.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/platform.dart';
import 'package:spotube/utils/service_utils.dart';
class WebViewLogin extends HookConsumerWidget { class WebViewLogin extends HookConsumerWidget {
const WebViewLogin({Key? key}) : super(key: key); const WebViewLogin({Key? key}) : super(key: key);
@ -14,7 +13,8 @@ class WebViewLogin extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mounted = useIsMounted(); final mounted = useIsMounted();
final auth = ref.watch(authProvider); final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier);
if (kIsDesktop) { if (kIsDesktop) {
const Scaffold( const Scaffold(
@ -62,11 +62,8 @@ class WebViewLogin extends HookConsumerWidget {
return previousValue; return previousValue;
}); });
final body = await ServiceUtils.getAccessToken(cookieHeader); authenticationNotifier.setCredentials(
auth.setAuthState( await AuthenticationCredentials.fromCookie(cookieHeader),
accessToken: body.accessToken,
authCookie: cookieHeader,
expiration: body.expiration,
); );
if (mounted()) { if (mounted()) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously

View File

@ -16,7 +16,7 @@ import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/hooks/use_breakpoints.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/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -34,7 +34,9 @@ class SearchPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { 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 spotify = ref.watch(spotifyProvider);
final albumController = useScrollController(); final albumController = useScrollController();
final playlistController = useScrollController(); final playlistController = useScrollController();
@ -83,7 +85,7 @@ class SearchPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
child: PlatformScaffold( child: PlatformScaffold(
appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null, appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null,
body: auth.isAnonymous body: !authenticationNotifier.isLoggedIn
? const AnonymousFallback() ? const AnonymousFallback()
: Column( : Column(
children: [ children: [

View File

@ -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/components/shared/page_window_title_bar.dart';
import 'package:spotube/main.dart'; import 'package:spotube/main.dart';
import 'package:spotube/collections/spotify_markets.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:spotube/provider/user_preferences_provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -21,7 +21,7 @@ class SettingsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final UserPreferences preferences = ref.watch(userPreferencesProvider); final UserPreferences preferences = ref.watch(userPreferencesProvider);
final Auth auth = ref.watch(authProvider); final auth = ref.watch(AuthenticationNotifier.provider);
final pickColorScheme = useCallback((ColorSchemeType schemeType) { final pickColorScheme = useCallback((ColorSchemeType schemeType) {
return () => showPlatformAlertDialog(context, builder: (context) { return () => showPlatformAlertDialog(context, builder: (context) {
@ -59,7 +59,7 @@ class SettingsPage extends HookConsumerWidget {
.headline .headline
?.copyWith(fontWeight: FontWeight.bold), ?.copyWith(fontWeight: FontWeight.bold),
), ),
if (auth.isAnonymous) if (auth == null)
AdaptiveListTile( AdaptiveListTile(
leading: Icon( leading: Icon(
SpotubeIcons.login, SpotubeIcons.login,
@ -93,10 +93,9 @@ class SettingsPage extends HookConsumerWidget {
child: PlatformText( child: PlatformText(
"Connect with Spotify".toUpperCase()), "Connect with Spotify".toUpperCase()),
), ),
), )
if (auth.isLoggedIn) else
Builder(builder: (context) { Builder(builder: (context) {
Auth auth = ref.watch(authProvider);
return PlatformListTile( return PlatformListTile(
leading: const Icon(SpotubeIcons.logout), leading: const Icon(SpotubeIcons.logout),
title: SizedBox( title: SizedBox(
@ -119,7 +118,10 @@ class SettingsPage extends HookConsumerWidget {
MaterialStateProperty.all(Colors.white), MaterialStateProperty.all(Colors.white),
), ),
onPressed: () async { onPressed: () async {
auth.logout(); ref
.read(
AuthenticationNotifier.provider.notifier)
.logout();
GoRouter.of(context).pop(); GoRouter.of(context).pop();
}, },
child: const PlatformText("Logout"), child: const PlatformText("Logout"),

View File

@ -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<void> 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<void> loadFromLocal(Map<String, dynamic> 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<Map<String, dynamic>> toMap() {
return {
"accessToken": _accessToken,
"expiration": _expiration.toString(),
"authCookie": _authCookie,
};
}
}
final authProvider = ChangeNotifierProvider<Auth>((ref) => Auth());

View File

@ -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<AuthenticationCredentials> 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<String, dynamic> json) {
return AuthenticationCredentials(
cookie: json['cookie'] as String,
accessToken: json['accessToken'] as String,
expiration: DateTime.parse(json['expiration'] as String),
);
}
Map<String, dynamic> 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<AuthenticationCredentials?> {
static final provider =
StateNotifierProvider<AuthenticationNotifier, AuthenticationCredentials?>(
(ref) => AuthenticationNotifier(),
);
bool get isLoggedIn => state != null;
AuthenticationNotifier() : super(null, "authentication");
Timer? _refreshTimer;
@override
FutureOr<void> 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<void> logout() async {
state = null;
if (kIsMobile) {
WebStorageManager.instance().android.deleteAllData();
CookieManager.instance().deleteAllCookies();
}
}
Future<void> refreshCredentials() async {
if (!isLoggedIn) {
return;
}
state = await AuthenticationCredentials.fromCookie(state!.cookie);
}
@override
FutureOr<AuthenticationCredentials?> fromJson(Map<String, dynamic> json) {
return AuthenticationCredentials.fromJson(json);
}
@override
Map<String, dynamic> toJson() {
return state?.toJson() ?? {};
}
}

View File

@ -1,14 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/generated_secrets.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'; import 'package:spotube/utils/primitive_utils.dart';
final spotifyProvider = Provider<SpotifyApi>((ref) { final spotifyProvider = Provider<SpotifyApi>((ref) {
Auth authState = ref.watch(authProvider); final authState = ref.watch(AuthenticationNotifier.provider);
final anonCred = PrimitiveUtils.getRandomElement(spotifySecrets); final anonCred = PrimitiveUtils.getRandomElement(spotifySecrets);
if (authState.isAnonymous) { if (authState == null) {
return SpotifyApi( return SpotifyApi(
SpotifyApiCredentials( SpotifyApiCredentials(
anonCred["clientId"], anonCred["clientId"],
@ -17,5 +17,5 @@ final spotifyProvider = Provider<SpotifyApi>((ref) {
); );
} }
return SpotifyApi.withAccessToken(authState.accessToken!); return SpotifyApi.withAccessToken(authState.accessToken);
}); });

View File

@ -6,8 +6,13 @@ import 'package:hive/hive.dart';
abstract class PersistedStateNotifier<T> extends StateNotifier<T> { abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
final String cacheKey; final String cacheKey;
PersistedStateNotifier(super.state, this.cacheKey) { FutureOr<void> onInit() {}
_load();
PersistedStateNotifier(
super.state,
this.cacheKey,
) {
_load().then((_) => onInit());
} }
Future<void> _load() async { Future<void> _load() async {

View File

@ -8,7 +8,6 @@ import 'package:spotube/components/library/user_local_tracks.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:spotube/models/lyrics.dart'; 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/spotube_track.dart';
import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
@ -246,23 +245,6 @@ abstract class ServiceUtils {
return subtitle; return subtitle;
} }
static Future<SpotifySpotubeCredentials> 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}) { static void navigate(BuildContext context, String location, {Object? extra}) {
GoRouter.of(context).push(location, extra: extra); GoRouter.of(context).push(location, extra: extra);
} }