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: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();

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/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) {

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/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();
}

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/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();
}

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/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 ?? [])
],

View File

@ -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(),
)

View File

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

View File

@ -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(

View File

@ -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,

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/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<T> 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<T> 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<T> extends HookConsumerWidget {
child: searchbar,
);
});
Overlay.of(context)?.insert(entry!);
Overlay.of(context).insert(entry!);
}
});
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/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

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_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(

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/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<State<IntroductionScreen>>();
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,

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/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,

View File

@ -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

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/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: [

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/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"),

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: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<SpotifyApi>((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<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> {
final String cacheKey;
PersistedStateNotifier(super.state, this.cacheKey) {
_load();
FutureOr<void> onInit() {}
PersistedStateNotifier(
super.state,
this.cacheKey,
) {
_load().then((_) => onInit());
}
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: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<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}) {
GoRouter.of(context).push(location, extra: extra);
}