diff --git a/android/build.gradle b/android/build.gradle index 4256f9173..a4d7066f4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afd..562c5e444 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/assets/tutorial/step-1.png b/assets/tutorial/step-1.png index 2338613c0..1182f0543 100644 Binary files a/assets/tutorial/step-1.png and b/assets/tutorial/step-1.png differ diff --git a/assets/tutorial/step-2.png b/assets/tutorial/step-2.png index ee8e1630e..af4616b0a 100644 Binary files a/assets/tutorial/step-2.png and b/assets/tutorial/step-2.png differ diff --git a/assets/tutorial/step-3.png b/assets/tutorial/step-3.png new file mode 100644 index 000000000..ddbea1406 Binary files /dev/null and b/assets/tutorial/step-3.png differ diff --git a/assets/tutorial/step-3a.jpg b/assets/tutorial/step-3a.jpg deleted file mode 100644 index 594b6b34c..000000000 Binary files a/assets/tutorial/step-3a.jpg and /dev/null differ diff --git a/assets/tutorial/step-3b.jpg b/assets/tutorial/step-3b.jpg deleted file mode 100644 index 6d45790c2..000000000 Binary files a/assets/tutorial/step-3b.jpg and /dev/null differ diff --git a/assets/tutorial/step-4.jpg b/assets/tutorial/step-4.jpg deleted file mode 100644 index 22db13d1d..000000000 Binary files a/assets/tutorial/step-4.jpg and /dev/null differ diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index f26a66e93..16a3934e1 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -26,19 +26,6 @@ import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/platform.dart'; -List spotifyScopes = [ - "playlist-modify-public", - "playlist-modify-private", - "playlist-read-private", - "user-library-read", - "user-library-modify", - "user-read-private", - "user-read-email", - "user-follow-read", - "user-follow-modify", - "playlist-read-collaborative" -]; - final selectedIndexState = StateProvider((ref) => 0); class Home extends HookConsumerWidget { diff --git a/lib/components/Login/LoginTutorial.dart b/lib/components/Login/LoginTutorial.dart index 7c9de9c90..f217e52f0 100644 --- a/lib/components/Login/LoginTutorial.dart +++ b/lib/components/Login/LoginTutorial.dart @@ -1,10 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:introduction_screen/introduction_screen.dart'; -import 'package:spotube/components/Login/LoginForm.dart'; +import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/provider/Auth.dart'; @@ -30,12 +28,12 @@ class LoginTutorial extends ConsumerWidget { back: const Text("Previous"), showBackButton: true, overrideDone: TextButton( - child: const Text("Done"), onPressed: auth.isLoggedIn ? () { GoRouter.of(context).go("/"); } : null, + child: const Text("Done"), ), pages: [ PageViewModel( @@ -48,12 +46,12 @@ class LoginTutorial extends ConsumerWidget { style: Theme.of(context).textTheme.bodyText1, ), Hyperlink( - "developer.spotify.com/dashboard ", - "https://developer.spotify.com/dashboard", + "accounts.spotify.com ", + "https://accounts.spotify.com", style: Theme.of(context).textTheme.bodyText1!, ), Text( - "and Login if you're not logged in", + "and Login/Sign up if you're not logged in", style: Theme.of(context).textTheme.bodyText1, ), ], @@ -63,89 +61,21 @@ class LoginTutorial extends ConsumerWidget { title: "Step 2", image: Image.asset("assets/tutorial/step-2.png"), bodyWidget: Text( - "Now, create an Spotify Developer Application by Clicking on the \"CREATE AN APP\" button. Give it a name and description too", + "1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection", textAlign: TextAlign.left, style: Theme.of(context).textTheme.bodyText1, ), ), PageViewModel( - title: "Step 3 [Really Important!]", - bodyWidget: Column( - children: [ - Text( - "Tap on the \"EDIT SETTINGS\" Button & navigate to \"Redirect URIs\" section", - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyText1, - ), - const SizedBox(height: 10), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - "Add ", - style: Theme.of(context).textTheme.bodyText1, - ), - OutlinedButton( - child: Text( - "http://localhost:4304/auth/spotify/callback", - style: Theme.of(context).textTheme.bodyText1?.copyWith( - color: Theme.of(context).primaryColor, - ), - ), - style: OutlinedButton.styleFrom( - shape: const RoundedRectangleBorder(), - ), - onPressed: () async { - await Clipboard.setData( - const ClipboardData( - text: "http://localhost:4304/auth/spotify/callback", - ), - ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Copied http://localhost:4304/auth/spotify/callback to clipboard", - textAlign: TextAlign.center, - ), - ), - ); - }, - ), - Text( - " to \"Redirect URIs\"", - style: Theme.of(context).textTheme.bodyText1, - ), - ], - ), - const SizedBox(height: 10), - Wrap( - runSpacing: 10, - spacing: 10, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), - child: Image.asset( - "assets/tutorial/step-3a.jpg", - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 700), - child: Image.asset( - "assets/tutorial/step-3b.jpg", - ), - ), - ], - ), - ], + title: "Step 3", + image: Image.asset( + "assets/tutorial/step-3.png", + ), + bodyWidget: Text( + "Copy the values of \"sp_dc\" and \"sp_key\" Cookies", + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyText1, ), - ), - PageViewModel( - title: "Step 4", - image: Image.asset("assets/tutorial/step-4.jpg"), - body: - "Finally, reveal the \"Client Secret\" by clicking on the \"SHOW CLIENT SECRET\" text\n Copy the Client ID & Client Secret then Paste them in the next Screen", ), if (auth.isLoggedIn) PageViewModel( @@ -163,11 +93,11 @@ class LoginTutorial extends ConsumerWidget { bodyWidget: Column( children: [ Text( - "Paste the Copied \"Client ID\" and \"Client Secret\" Here", + "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields", style: Theme.of(context).textTheme.bodyText1, ), const SizedBox(height: 10), - LoginForm(), + const TokenLoginForm(), ], ), ), diff --git a/lib/components/Login/Login.dart b/lib/components/Login/TokenLogin.dart similarity index 87% rename from lib/components/Login/Login.dart rename to lib/components/Login/TokenLogin.dart index 8492cf916..57b2be9ad 100644 --- a/lib/components/Login/Login.dart +++ b/lib/components/Login/TokenLogin.dart @@ -1,19 +1,16 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/Login/LoginForm.dart'; +import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/models/Logger.dart'; -class Login extends HookConsumerWidget { - Login({Key? key}) : super(key: key); - final log = getLogger(Login); +class TokenLogin extends HookConsumerWidget { + const TokenLogin({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { final breakpoint = useBreakpoints(); - final textTheme = Theme.of(context).textTheme; return SafeArea( @@ -39,8 +36,8 @@ class Login extends HookConsumerWidget { style: Theme.of(context).textTheme.caption, ), const SizedBox(height: 10), - LoginForm( - onDone: () => GoRouter.of(context).pop(), + TokenLoginForm( + onDone: () => GoRouter.of(context).go("/"), ), const SizedBox(height: 10), Wrap( diff --git a/lib/components/Login/LoginForm.dart b/lib/components/Login/TokenLoginForms.dart similarity index 51% rename from lib/components/Login/LoginForm.dart rename to lib/components/Login/TokenLoginForms.dart index e2078b12c..f50e25db3 100644 --- a/lib/components/Login/LoginForm.dart +++ b/lib/components/Login/TokenLoginForms.dart @@ -1,40 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/utils/service_utils.dart'; -class LoginForm extends HookConsumerWidget { +class TokenLoginForm extends HookConsumerWidget { final void Function()? onDone; - LoginForm({this.onDone, Key? key}) : super(key: key); - - final log = getLogger(LoginForm); + const TokenLoginForm({ + Key? key, + this.onDone, + }) : super(key: key); @override Widget build(BuildContext context, ref) { Auth authState = ref.watch(authProvider); - final clientIdController = useTextEditingController(); - final clientSecretController = useTextEditingController(); - final fieldError = useState(false); - - Future handleLogin(Auth authState) async { - try { - if (clientIdController.value.text == "" || - clientSecretController.value.text == "") { - fieldError.value = true; - } - await ServiceUtils.oauthLogin( - ref.read(authProvider), - clientId: clientIdController.value.text, - clientSecret: clientSecretController.value.text, - ).then( - (value) => onDone?.call(), - ); - } catch (e) { - log.e("[Login.handleLogin] $e"); - } - } + final directCodeController = useTextEditingController(); + final keyCodeController = useTextEditingController(); + final mounted = useIsMounted(); return ConstrainedBox( constraints: const BoxConstraints( @@ -43,27 +25,27 @@ class LoginForm extends HookConsumerWidget { child: Column( children: [ TextField( - controller: clientIdController, + controller: directCodeController, decoration: const InputDecoration( - hintText: "Spotify Client ID", - label: Text("ClientID"), + hintText: "Spotify \"sp_dc\" Cookie", + label: Text("sp_dc Cookie"), ), keyboardType: TextInputType.visiblePassword, ), const SizedBox(height: 10), TextField( - controller: clientSecretController, + controller: keyCodeController, decoration: const InputDecoration( - hintText: "Spotify Client Secret", - label: Text("Client Secret"), + hintText: "Spotify \"sp_key\" Cookie", + label: Text("sp_key Cookie"), ), keyboardType: TextInputType.visiblePassword, ), const SizedBox(height: 20), ElevatedButton( onPressed: () async { - if (clientSecretController.text.isEmpty || - clientIdController.text.isEmpty) { + if (keyCodeController.text.isEmpty || + directCodeController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Please fill in all fields"), @@ -72,7 +54,18 @@ class LoginForm extends HookConsumerWidget { ); return; } - await handleLogin(authState); + 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, + ); + if (mounted()) { + onDone?.call(); + } }, child: const Text("Submit"), ) diff --git a/lib/components/Login/WebViewLogin.dart b/lib/components/Login/WebViewLogin.dart new file mode 100644 index 000000000..bb60ede68 --- /dev/null +++ b/lib/components/Login/WebViewLogin.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/Auth.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); + + @override + Widget build(BuildContext context, ref) { + final mounted = useIsMounted(); + final auth = ref.watch(authProvider); + + if (kIsDesktop) { + const Scaffold( + body: Center( + child: Text('This feature is not available on desktop'), + ), + ); + } + + return Scaffold( + body: SafeArea( + child: InAppWebView( + initialUrlRequest: URLRequest( + url: Uri.parse("https://accounts.spotify.com/"), + ), + androidOnPermissionRequest: (controller, origin, resources) async { + return PermissionRequestResponse( + resources: resources, + action: PermissionRequestResponseAction.GRANT); + }, + onLoadStop: (controller, action) async { + if (action == null) return; + String url = action.toString(); + if (url.endsWith("/")) { + url = url.substring(0, url.length - 1); + } + + if (url == "https://accounts.spotify.com/en/status") { + final cookies = + await CookieManager.instance().getCookies(url: action); + final cookieHeader = + cookies.fold("", (previousValue, element) { + if (element.name == "sp_dc" || element.name == "sp_key") { + return "$previousValue; ${element.name}=${element.value}"; + } + return previousValue; + }); + + final body = await ServiceUtils.getAccessToken(cookieHeader); + auth.setAuthState( + accessToken: body.accessToken, + authCookie: cookieHeader, + expiration: body.expiration, + ); + if (mounted()) { + GoRouter.of(context).go("/"); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index e55c2b7b8..994f12c8c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -166,10 +166,12 @@ class _SpotubeState extends ConsumerState with WidgetsBindingObserver { @override void didChangeMetrics() { super.didChangeMetrics(); - if (localStorage == null || - (prevSize?.width == appWindow.size.width && - prevSize?.height == appWindow.size.height) || - kIsMobile) return; + final windowSameDimension = kIsMobile + ? false + : prevSize?.width == appWindow.size.width && + prevSize?.height == appWindow.size.height; + + if (localStorage == null || windowSameDimension || kIsMobile) return; localStorage!.setString( LocalStorageKeys.windowSizeInfo, jsonEncode({ diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index 2f6b1b959..0db6a56d1 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -3,12 +3,14 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Home/Home.dart'; -import 'package:spotube/components/Login/Login.dart'; import 'package:spotube/components/Login/LoginTutorial.dart'; +import 'package:spotube/components/Login/TokenLogin.dart'; import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Settings/Settings.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:spotube/components/Login/WebViewLogin.dart'; GoRouter createGoRouter() => GoRouter( routes: [ @@ -19,7 +21,7 @@ GoRouter createGoRouter() => GoRouter( GoRoute( path: "/login", pageBuilder: (context, state) => SpotubePage( - child: Login(), + child: kIsMobile ? const WebViewLogin() : const TokenLogin(), ), ), GoRoute( diff --git a/lib/models/SpotifySpotubeCredentials.dart b/lib/models/SpotifySpotubeCredentials.dart new file mode 100644 index 000000000..982ca64a2 --- /dev/null +++ b/lib/models/SpotifySpotubeCredentials.dart @@ -0,0 +1,30 @@ +class SpotifySpotubeCredentials { + String clientId; + String accessToken; + DateTime expiration; + bool isAnonymous; + + SpotifySpotubeCredentials({ + required this.clientId, + required this.accessToken, + required this.expiration, + required this.isAnonymous, + }); + + SpotifySpotubeCredentials.fromJson(Map json) + : clientId = json['clientId'], + accessToken = json['accessToken'], + expiration = DateTime.fromMillisecondsSinceEpoch( + json['accessTokenExpirationTimestampMs'], + ), + isAnonymous = json['isAnonymous']; + + Map toJson() { + return { + 'clientId': clientId, + 'accessToken': accessToken, + 'accessTokenExpirationTimestampMs': expiration.millisecondsSinceEpoch, + 'isAnonymous': isAnonymous, + }; + } +} diff --git a/lib/provider/Auth.dart b/lib/provider/Auth.dart index 40cdc4e25..c5981be89 100644 --- a/lib/provider/Auth.dart +++ b/lib/provider/Auth.dart @@ -1,90 +1,117 @@ import 'dart:async'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/utils/PersistedChangeNotifier.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/service_utils.dart'; class Auth extends PersistedChangeNotifier { - String? _clientId; - String? _clientSecret; String? _accessToken; - String? _refreshToken; DateTime? _expiration; + String? _authCookie; - Auth() : super(); + Timer? _refresher; + + Auth() : super() { + _refresher = _createRefresher(); + } - String? get clientId => _clientId; - String? get clientSecret => _clientSecret; String? get accessToken => _accessToken; - String? get refreshToken => _refreshToken; DateTime? get expiration => _expiration; + String? get authCookie => _authCookie; - bool get isAnonymous => - _clientId == null && - _clientSecret == null && - accessToken == null && - refreshToken == null; + 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; + + _refresh() async { + final data = await ServiceUtils.getAccessToken(authCookie!); + _accessToken = data.accessToken; + _expiration = data.expiration; + _restartRefresher(); + } + + Timer? _createRefresher() { + if (expiration == null || !isExpired || authCookie == null) { + return null; + } + _refresher?.cancel(); + return Timer(expiresIn, _refresh); + } + + void _restartRefresher() { + _refresher?.cancel(); + _refresher = _createRefresher(); + } void setAuthState({ bool safe = true, - String? clientId, - String? clientSecret, - String? refreshToken, String? accessToken, DateTime? expiration, + String? authCookie, }) { if (safe) { - if (clientId != null) _clientId = clientId; - if (clientSecret != null) _clientSecret = clientSecret; - if (refreshToken != null) _refreshToken = refreshToken; if (accessToken != null) _accessToken = accessToken; - if (expiration != null) _expiration = expiration; + if (expiration != null) { + _expiration = expiration; + _restartRefresher(); + } + if (authCookie != null) _authCookie = authCookie; } else { - _clientId = clientId; - _clientSecret = clientSecret; _accessToken = accessToken; - _refreshToken = refreshToken; _expiration = expiration; + _authCookie = authCookie; + + _restartRefresher(); } notifyListeners(); updatePersistence(); } void logout() { - _clientId = null; - _clientSecret = null; _accessToken = null; - _refreshToken = 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(clientId: $clientId, clientSecret: $clientSecret, accessToken: $accessToken, refreshToken: $refreshToken, expiration: $expiration, isLoggedIn: $isLoggedIn)"; + return "Auth(accessToken: $accessToken, expiration: $expiration, isLoggedIn: $isLoggedIn, isAnonymous: $isAnonymous, authCookie: $authCookie)"; } @override FutureOr loadFromLocal(Map map) { - _clientId = map["clientId"]; - _clientSecret = map["clientSecret"]; _accessToken = map["accessToken"]; - _refreshToken = map["refreshToken"]; _expiration = map["expiration"] != null ? DateTime.tryParse(map["expiration"]) : _expiration; + _authCookie = map["authCookie"]; + _restartRefresher(); + if (isExpired) { + _refresh(); + } } @override FutureOr> toMap() { return { - "clientId": _clientId, - "clientSecret": _clientSecret, "accessToken": _accessToken, - "refreshToken": _refreshToken, "expiration": _expiration.toString(), + "authCookie": _authCookie, }; } } diff --git a/lib/provider/SpotifyDI.dart b/lib/provider/SpotifyDI.dart index 9e2433d00..16a4b03f8 100644 --- a/lib/provider/SpotifyDI.dart +++ b/lib/provider/SpotifyDI.dart @@ -1,6 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/utils/primitive_utils.dart'; @@ -8,30 +7,15 @@ import 'package:spotube/utils/primitive_utils.dart'; final spotifyProvider = Provider((ref) { Auth authState = ref.watch(authProvider); final anonCred = PrimitiveUtils.getRandomElement(spotifySecrets); - SpotifyApiCredentials apiCredentials = authState.isAnonymous - ? SpotifyApiCredentials( - anonCred["clientId"], - anonCred["clientSecret"], - ) - : SpotifyApiCredentials( - authState.clientId, - authState.clientSecret, - accessToken: authState.accessToken, - refreshToken: authState.refreshToken, - expiration: authState.expiration, - scopes: spotifyScopes, - ); - return SpotifyApi( - apiCredentials, - onCredentialsRefreshed: (credentials) { - authState.setAuthState( - clientId: credentials.clientId, - clientSecret: credentials.clientSecret, - accessToken: credentials.accessToken, - refreshToken: credentials.refreshToken, - expiration: credentials.expiration, - ); - }, - ); + if (authState.isAnonymous) { + return SpotifyApi( + SpotifyApiCredentials( + anonCred["clientId"], + anonCred["clientSecret"], + ), + ); + } + + return SpotifyApi.withAccessToken(authState.accessToken!); }); diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 574899c88..12fa5b20a 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -4,11 +4,11 @@ import 'dart:io'; import 'package:html/dom.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/Logger.dart'; import 'package:http/http.dart' as http; import 'package:spotube/models/LyricsModels.dart'; +import 'package:spotube/models/SpotifySpotubeCredentials.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/provider/Auth.dart'; @@ -176,6 +176,7 @@ abstract class ServiceUtils { } } + @Deprecated("Use getAccessToken instead") static Future connectIpc(String authUri, String redirectUri) async { try { logger.i("[connectIpc][Launching]: $authUri"); @@ -220,6 +221,9 @@ abstract class ServiceUtils { static const authRedirectUri = "http://localhost:4304/auth/spotify/callback"; + /// Use [getAccessToken] instead + /// This method will be removed in the next major release + @Deprecated("Use getAccessToken instead") static Future oauthLogin(Auth auth, {required String clientId, required String clientSecret}) async { try { @@ -229,8 +233,9 @@ abstract class ServiceUtils { final credentials = SpotifyApiCredentials(clientId, clientSecret); final grant = SpotifyApi.authorizationCodeGrant(credentials); - final authUri = grant.getAuthorizationUrl(Uri.parse(authRedirectUri), - scopes: spotifyScopes); + final authUri = grant.getAuthorizationUrl( + Uri.parse(authRedirectUri), + ); final responseUri = await connectIpc(authUri.toString(), authRedirectUri); SharedPreferences localStorage = await SharedPreferences.getInstance(); @@ -261,13 +266,13 @@ abstract class ServiceUtils { clientSecret, ); - auth.setAuthState( - clientId: clientId, - clientSecret: clientSecret, - accessToken: accessToken, - refreshToken: refreshToken, - expiration: expiration, - ); + // auth.setAuthState( + // clientId: clientId, + // clientSecret: clientSecret, + // accessToken: accessToken, + // refreshToken: refreshToken, + // expiration: expiration, + // ); } catch (e, stack) { logger.e("oauthLogin", e, stack); rethrow; @@ -360,4 +365,26 @@ abstract class ServiceUtils { return subtitle; } + + static Future getAccessToken( + String cookieHeader) async { + try { + 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), + ); + } catch (e, stack) { + logger.e("getAccessToken", e, stack); + rethrow; + } + } } diff --git a/pubspec.lock b/pubspec.lock index bdfe55565..14ead2658 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -540,6 +540,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.18.5+1" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + url: "https://pub.dartlang.org" + source: hosted + version: "5.4.3+7" flutter_launcher_icons: dependency: "direct dev" description: @@ -641,13 +648,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.3" - hookified_infinite_scroll_pagination: - dependency: "direct main" - description: - name: hookified_infinite_scroll_pagination - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.0" hooks_riverpod: dependency: "direct main" description: @@ -690,13 +690,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.0" - infinite_scroll_pagination: - dependency: transitive - description: - name: infinite_scroll_pagination - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.0" introduction_screen: dependency: "direct main" description: @@ -1129,13 +1122,6 @@ packages: description: flutter source: sdk version: "0.0.99" - sliver_tools: - dependency: transitive - description: - name: sliver_tools - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.6" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 61f672017..89e3bad9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: visibility_detector: ^0.3.3 fl_query: ^0.3.0 fl_query_hooks: ^0.3.0 + flutter_inappwebview: ^5.4.3+7 dev_dependencies: flutter_test: