feat(auth): new authentication flow using cookies and webview in android

This commit is contained in:
Kingkor Roy Tirtho 2022-10-03 20:38:36 +06:00
parent 139d4dc033
commit 756b91007e
21 changed files with 275 additions and 239 deletions

View File

@ -6,7 +6,7 @@ buildscript {
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 744 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 149 KiB

BIN
assets/tutorial/step-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -26,19 +26,6 @@ import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
List<String> 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); final selectedIndexState = StateProvider((ref) => 0);
class Home extends HookConsumerWidget { class Home extends HookConsumerWidget {

View File

@ -1,10 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:introduction_screen/introduction_screen.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/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
@ -30,12 +28,12 @@ class LoginTutorial extends ConsumerWidget {
back: const Text("Previous"), back: const Text("Previous"),
showBackButton: true, showBackButton: true,
overrideDone: TextButton( overrideDone: TextButton(
child: const Text("Done"),
onPressed: auth.isLoggedIn onPressed: auth.isLoggedIn
? () { ? () {
GoRouter.of(context).go("/"); GoRouter.of(context).go("/");
} }
: null, : null,
child: const Text("Done"),
), ),
pages: [ pages: [
PageViewModel( PageViewModel(
@ -48,12 +46,12 @@ class LoginTutorial extends ConsumerWidget {
style: Theme.of(context).textTheme.bodyText1, style: Theme.of(context).textTheme.bodyText1,
), ),
Hyperlink( Hyperlink(
"developer.spotify.com/dashboard ", "accounts.spotify.com ",
"https://developer.spotify.com/dashboard", "https://accounts.spotify.com",
style: Theme.of(context).textTheme.bodyText1!, style: Theme.of(context).textTheme.bodyText1!,
), ),
Text( 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, style: Theme.of(context).textTheme.bodyText1,
), ),
], ],
@ -63,89 +61,21 @@ class LoginTutorial extends ConsumerWidget {
title: "Step 2", title: "Step 2",
image: Image.asset("assets/tutorial/step-2.png"), image: Image.asset("assets/tutorial/step-2.png"),
bodyWidget: Text( 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, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1, style: Theme.of(context).textTheme.bodyText1,
), ),
), ),
PageViewModel( PageViewModel(
title: "Step 3 [Really Important!]", title: "Step 3",
bodyWidget: Column( image: Image.asset(
children: [ "assets/tutorial/step-3.png",
Text( ),
"Tap on the \"EDIT SETTINGS\" Button & navigate to \"Redirect URIs\" section", bodyWidget: Text(
textAlign: TextAlign.left, "Copy the values of \"sp_dc\" and \"sp_key\" Cookies",
style: Theme.of(context).textTheme.bodyText1, 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",
),
),
],
),
],
), ),
),
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) if (auth.isLoggedIn)
PageViewModel( PageViewModel(
@ -163,11 +93,11 @@ class LoginTutorial extends ConsumerWidget {
bodyWidget: Column( bodyWidget: Column(
children: [ children: [
Text( 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, style: Theme.of(context).textTheme.bodyText1,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
LoginForm(), const TokenLoginForm(),
], ],
), ),
), ),

View File

@ -1,19 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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:spotube/components/Login/LoginForm.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/Logger.dart';
class Login extends HookConsumerWidget { class TokenLogin extends HookConsumerWidget {
Login({Key? key}) : super(key: key); const TokenLogin({Key? key}) : super(key: key);
final log = getLogger(Login);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return SafeArea( return SafeArea(
@ -39,8 +36,8 @@ class Login extends HookConsumerWidget {
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
LoginForm( TokenLoginForm(
onDone: () => GoRouter.of(context).pop(), onDone: () => GoRouter.of(context).go("/"),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Wrap( Wrap(

View File

@ -1,40 +1,22 @@
import 'package:flutter/material.dart'; 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:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class LoginForm extends HookConsumerWidget { class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone; final void Function()? onDone;
LoginForm({this.onDone, Key? key}) : super(key: key); const TokenLoginForm({
Key? key,
final log = getLogger(LoginForm); this.onDone,
}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Auth authState = ref.watch(authProvider); Auth authState = ref.watch(authProvider);
final clientIdController = useTextEditingController(); final directCodeController = useTextEditingController();
final clientSecretController = useTextEditingController(); final keyCodeController = useTextEditingController();
final fieldError = useState(false); final mounted = useIsMounted();
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");
}
}
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -43,27 +25,27 @@ class LoginForm extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
TextField( TextField(
controller: clientIdController, controller: directCodeController,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "Spotify Client ID", hintText: "Spotify \"sp_dc\" Cookie",
label: Text("ClientID"), label: Text("sp_dc Cookie"),
), ),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: clientSecretController, controller: keyCodeController,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "Spotify Client Secret", hintText: "Spotify \"sp_key\" Cookie",
label: Text("Client Secret"), label: Text("sp_key Cookie"),
), ),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
if (clientSecretController.text.isEmpty || if (keyCodeController.text.isEmpty ||
clientIdController.text.isEmpty) { directCodeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Please fill in all fields"), content: Text("Please fill in all fields"),
@ -72,7 +54,18 @@ class LoginForm extends HookConsumerWidget {
); );
return; 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"), child: const Text("Submit"),
) )

View File

@ -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<String>("", (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("/");
}
}
},
),
),
);
}
}

View File

@ -166,10 +166,12 @@ class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
@override @override
void didChangeMetrics() { void didChangeMetrics() {
super.didChangeMetrics(); super.didChangeMetrics();
if (localStorage == null || final windowSameDimension = kIsMobile
(prevSize?.width == appWindow.size.width && ? false
prevSize?.height == appWindow.size.height) || : prevSize?.width == appWindow.size.width &&
kIsMobile) return; prevSize?.height == appWindow.size.height;
if (localStorage == null || windowSameDimension || kIsMobile) return;
localStorage!.setString( localStorage!.setString(
LocalStorageKeys.windowSizeInfo, LocalStorageKeys.windowSizeInfo,
jsonEncode({ jsonEncode({

View File

@ -3,12 +3,14 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart';
import 'package:spotube/components/Home/Home.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/LoginTutorial.dart';
import 'package:spotube/components/Login/TokenLogin.dart';
import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Player/PlayerView.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Settings/Settings.dart'; import 'package:spotube/components/Settings/Settings.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/Login/WebViewLogin.dart';
GoRouter createGoRouter() => GoRouter( GoRouter createGoRouter() => GoRouter(
routes: [ routes: [
@ -19,7 +21,7 @@ GoRouter createGoRouter() => GoRouter(
GoRoute( GoRoute(
path: "/login", path: "/login",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => SpotubePage(
child: Login(), child: kIsMobile ? const WebViewLogin() : const TokenLogin(),
), ),
), ),
GoRoute( GoRoute(

View File

@ -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<String, dynamic> json)
: clientId = json['clientId'],
accessToken = json['accessToken'],
expiration = DateTime.fromMillisecondsSinceEpoch(
json['accessTokenExpirationTimestampMs'],
),
isAnonymous = json['isAnonymous'];
Map<String, dynamic> toJson() {
return <String, dynamic>{
'clientId': clientId,
'accessToken': accessToken,
'accessTokenExpirationTimestampMs': expiration.millisecondsSinceEpoch,
'isAnonymous': isAnonymous,
};
}
}

View File

@ -1,90 +1,117 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/utils/PersistedChangeNotifier.dart'; import 'package:spotube/utils/PersistedChangeNotifier.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
class Auth extends PersistedChangeNotifier { class Auth extends PersistedChangeNotifier {
String? _clientId;
String? _clientSecret;
String? _accessToken; String? _accessToken;
String? _refreshToken;
DateTime? _expiration; 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 accessToken => _accessToken;
String? get refreshToken => _refreshToken;
DateTime? get expiration => _expiration; DateTime? get expiration => _expiration;
String? get authCookie => _authCookie;
bool get isAnonymous => bool get isAnonymous => accessToken == null && authCookie == null;
_clientId == null &&
_clientSecret == null &&
accessToken == null &&
refreshToken == null;
bool get isLoggedIn => !isAnonymous && _expiration != 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({ void setAuthState({
bool safe = true, bool safe = true,
String? clientId,
String? clientSecret,
String? refreshToken,
String? accessToken, String? accessToken,
DateTime? expiration, DateTime? expiration,
String? authCookie,
}) { }) {
if (safe) { if (safe) {
if (clientId != null) _clientId = clientId;
if (clientSecret != null) _clientSecret = clientSecret;
if (refreshToken != null) _refreshToken = refreshToken;
if (accessToken != null) _accessToken = accessToken; if (accessToken != null) _accessToken = accessToken;
if (expiration != null) _expiration = expiration; if (expiration != null) {
_expiration = expiration;
_restartRefresher();
}
if (authCookie != null) _authCookie = authCookie;
} else { } else {
_clientId = clientId;
_clientSecret = clientSecret;
_accessToken = accessToken; _accessToken = accessToken;
_refreshToken = refreshToken;
_expiration = expiration; _expiration = expiration;
_authCookie = authCookie;
_restartRefresher();
} }
notifyListeners(); notifyListeners();
updatePersistence(); updatePersistence();
} }
void logout() { void logout() {
_clientId = null;
_clientSecret = null;
_accessToken = null; _accessToken = null;
_refreshToken = null;
_expiration = null; _expiration = null;
_authCookie = null;
_refresher?.cancel();
_refresher = null;
if (kIsMobile) {
WebStorageManager.instance().android.deleteAllData();
CookieManager.instance().deleteAllCookies();
}
notifyListeners(); notifyListeners();
updatePersistence(clearNullEntries: true); updatePersistence(clearNullEntries: true);
} }
@override @override
String toString() { 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 @override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) { FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
_clientId = map["clientId"];
_clientSecret = map["clientSecret"];
_accessToken = map["accessToken"]; _accessToken = map["accessToken"];
_refreshToken = map["refreshToken"];
_expiration = map["expiration"] != null _expiration = map["expiration"] != null
? DateTime.tryParse(map["expiration"]) ? DateTime.tryParse(map["expiration"])
: _expiration; : _expiration;
_authCookie = map["authCookie"];
_restartRefresher();
if (isExpired) {
_refresh();
}
} }
@override @override
FutureOr<Map<String, dynamic>> toMap() { FutureOr<Map<String, dynamic>> toMap() {
return { return {
"clientId": _clientId,
"clientSecret": _clientSecret,
"accessToken": _accessToken, "accessToken": _accessToken,
"refreshToken": _refreshToken,
"expiration": _expiration.toString(), "expiration": _expiration.toString(),
"authCookie": _authCookie,
}; };
} }
} }

View File

@ -1,6 +1,5 @@
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/components/Home/Home.dart';
import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
@ -8,30 +7,15 @@ import 'package:spotube/utils/primitive_utils.dart';
final spotifyProvider = Provider<SpotifyApi>((ref) { final spotifyProvider = Provider<SpotifyApi>((ref) {
Auth authState = ref.watch(authProvider); Auth authState = ref.watch(authProvider);
final anonCred = PrimitiveUtils.getRandomElement(spotifySecrets); 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( if (authState.isAnonymous) {
apiCredentials, return SpotifyApi(
onCredentialsRefreshed: (credentials) { SpotifyApiCredentials(
authState.setAuthState( anonCred["clientId"],
clientId: credentials.clientId, anonCred["clientSecret"],
clientSecret: credentials.clientSecret, ),
accessToken: credentials.accessToken, );
refreshToken: credentials.refreshToken, }
expiration: credentials.expiration,
); return SpotifyApi.withAccessToken(authState.accessToken!);
},
);
}); });

View File

@ -4,11 +4,11 @@ import 'dart:io';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.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/LyricsModels.dart'; import 'package:spotube/models/LyricsModels.dart';
import 'package:spotube/models/SpotifySpotubeCredentials.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
@ -176,6 +176,7 @@ abstract class ServiceUtils {
} }
} }
@Deprecated("Use getAccessToken instead")
static Future<String?> connectIpc(String authUri, String redirectUri) async { static Future<String?> connectIpc(String authUri, String redirectUri) async {
try { try {
logger.i("[connectIpc][Launching]: $authUri"); logger.i("[connectIpc][Launching]: $authUri");
@ -220,6 +221,9 @@ abstract class ServiceUtils {
static const authRedirectUri = "http://localhost:4304/auth/spotify/callback"; 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<void> oauthLogin(Auth auth, static Future<void> oauthLogin(Auth auth,
{required String clientId, required String clientSecret}) async { {required String clientId, required String clientSecret}) async {
try { try {
@ -229,8 +233,9 @@ abstract class ServiceUtils {
final credentials = SpotifyApiCredentials(clientId, clientSecret); final credentials = SpotifyApiCredentials(clientId, clientSecret);
final grant = SpotifyApi.authorizationCodeGrant(credentials); final grant = SpotifyApi.authorizationCodeGrant(credentials);
final authUri = grant.getAuthorizationUrl(Uri.parse(authRedirectUri), final authUri = grant.getAuthorizationUrl(
scopes: spotifyScopes); Uri.parse(authRedirectUri),
);
final responseUri = await connectIpc(authUri.toString(), authRedirectUri); final responseUri = await connectIpc(authUri.toString(), authRedirectUri);
SharedPreferences localStorage = await SharedPreferences.getInstance(); SharedPreferences localStorage = await SharedPreferences.getInstance();
@ -261,13 +266,13 @@ abstract class ServiceUtils {
clientSecret, clientSecret,
); );
auth.setAuthState( // auth.setAuthState(
clientId: clientId, // clientId: clientId,
clientSecret: clientSecret, // clientSecret: clientSecret,
accessToken: accessToken, // accessToken: accessToken,
refreshToken: refreshToken, // refreshToken: refreshToken,
expiration: expiration, // expiration: expiration,
); // );
} catch (e, stack) { } catch (e, stack) {
logger.e("oauthLogin", e, stack); logger.e("oauthLogin", e, stack);
rethrow; rethrow;
@ -360,4 +365,26 @@ abstract class ServiceUtils {
return subtitle; return subtitle;
} }
static Future<SpotifySpotubeCredentials> 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;
}
}
} }

View File

@ -540,6 +540,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.18.5+1" 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: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -641,13 +648,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.3" 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: hooks_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@ -690,13 +690,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" 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: introduction_screen:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1129,13 +1122,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" 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: source_gen:
dependency: transitive dependency: transitive
description: description:

View File

@ -56,6 +56,7 @@ dependencies:
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
fl_query: ^0.3.0 fl_query: ^0.3.0
fl_query_hooks: ^0.3.0 fl_query_hooks: ^0.3.0
flutter_inappwebview: ^5.4.3+7
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: