From 15bd58a95597925637bf66866af59c2780aeff97 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 11:21:32 +0600 Subject: [PATCH] feat(desktop): implement webview based login --- lib/collections/routes.dart | 13 +- lib/main.dart | 2 +- lib/modules/desktop_login/login_form.dart | 69 ---------- lib/pages/desktop_login/desktop_login.dart | 78 ----------- lib/pages/desktop_login/login_tutorial.dart | 125 ------------------ lib/pages/settings/sections/accounts.dart | 44 ++++-- .../authentication/authentication.dart | 4 + linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec.lock | 9 ++ pubspec.yaml | 5 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 15 files changed, 73 insertions(+), 293 deletions(-) delete mode 100644 lib/modules/desktop_login/login_form.dart delete mode 100644 lib/pages/desktop_login/desktop_login.dart delete mode 100644 lib/pages/desktop_login/login_tutorial.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index b3cba581..3bf1d883 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -34,12 +34,9 @@ import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/spotube_page_route.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/pages/library/library.dart'; -import 'package:spotube/pages/desktop_login/login_tutorial.dart'; -import 'package:spotube/pages/desktop_login/desktop_login.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/root/root_app.dart'; import 'package:spotube/pages/settings/settings.dart'; @@ -313,16 +310,8 @@ final routerProvider = Provider((ref) { path: "/login", name: WebViewLogin.name, parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => SpotubePage( - child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(), - ), - ), - GoRoute( - path: "/login-tutorial", - name: LoginTutorial.name, - parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => const SpotubePage( - child: LoginTutorial(), + child: WebViewLogin(), ), ), GoRoute( diff --git a/lib/main.dart b/lib/main.dart index cb553115..b3a3132f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -65,7 +65,7 @@ Future main(List rawArgs) async { await FlutterDisplayMode.setHighRefreshRate(); } - if (kIsDesktop) { + if (kIsDesktop && !kIsMacOS) { await windowManager.setPreventClose(true); } diff --git a/lib/modules/desktop_login/login_form.dart b/lib/modules/desktop_login/login_form.dart deleted file mode 100644 index e5d31215..00000000 --- a/lib/modules/desktop_login/login_form.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/extensions/context.dart'; - -import 'package:spotube/provider/authentication/authentication.dart'; - -class TokenLoginForm extends HookConsumerWidget { - final void Function()? onDone; - const TokenLoginForm({ - super.key, - this.onDone, - }); - - @override - Widget build(BuildContext context, ref) { - final authenticationNotifier = ref.watch(authenticationProvider.notifier); - final directCodeController = useTextEditingController(); - - final isLoading = useState(false); - - return ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Column( - children: [ - TextField( - controller: directCodeController, - decoration: InputDecoration( - hintText: context.l10n.spotify_cookie("\"sp_dc\""), - labelText: context.l10n.cookie_name_cookie("sp_dc"), - ), - keyboardType: TextInputType.visiblePassword, - ), - const SizedBox(height: 10), - FilledButton( - onPressed: isLoading.value - ? null - : () async { - try { - isLoading.value = true; - if (directCodeController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.fill_in_all_fields), - behavior: SnackBarBehavior.floating, - ), - ); - return; - } - final cookieHeader = - "sp_dc=${directCodeController.text.trim()}"; - - await authenticationNotifier.login(cookieHeader); - if (context.mounted) { - onDone?.call(); - } - } finally { - isLoading.value = false; - } - }, - child: Text(context.l10n.submit), - ) - ], - ), - ); - } -} diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart deleted file mode 100644 index 80548898..00000000 --- a/lib/pages/desktop_login/desktop_login.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/modules/desktop_login/login_form.dart'; -import 'package:spotube/components/titlebar/titlebar.dart'; -import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/pages/mobile_login/mobile_login.dart'; - -class DesktopLoginPage extends HookConsumerWidget { - static const name = WebViewLogin.name; - const DesktopLoginPage({super.key}); - - @override - Widget build(BuildContext context, ref) { - final mediaQuery = MediaQuery.of(context); - final theme = Theme.of(context); - final color = theme.colorScheme.surfaceVariant.withOpacity(.3); - - return SafeArea( - child: Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), - ), - body: SingleChildScrollView( - child: Center( - child: Container( - margin: const EdgeInsets.all(10), - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(10), - ), - child: Column( - children: [ - Assets.spotubeLogoPng.image( - width: MediaQuery.of(context).size.width * - (mediaQuery.mdAndDown ? .5 : .3), - ), - Text( - context.l10n.add_spotify_credentials, - style: theme.textTheme.titleMedium, - ), - Text( - context.l10n.credentials_will_not_be_shared_disclaimer, - style: theme.textTheme.labelMedium, - ), - const SizedBox(height: 10), - TokenLoginForm( - onDone: () => GoRouter.of(context).go("/"), - ), - const SizedBox(height: 10), - Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text(context.l10n.know_how_to_login), - TextButton( - child: Text( - context.l10n.follow_step_by_step_guide, - ), - onPressed: () => GoRouter.of(context).push( - "/login-tutorial", - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart deleted file mode 100644 index ec62543c..00000000 --- a/lib/pages/desktop_login/login_tutorial.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/material.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/collections/assets.gen.dart'; -import 'package:spotube/modules/desktop_login/login_form.dart'; -import 'package:spotube/components/links/hyper_link.dart'; -import 'package:spotube/components/titlebar/titlebar.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/pages/home/home.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/utils/service_utils.dart'; - -class LoginTutorial extends ConsumerWidget { - static const name = "login_tutorial"; - const LoginTutorial({super.key}); - - @override - Widget build(BuildContext context, ref) { - final auth = ref.watch(authenticationProvider); - final key = GlobalKey>(); - final theme = Theme.of(context); - - final pageDecoration = PageDecoration( - bodyTextStyle: theme.textTheme.bodyMedium!, - titleTextStyle: theme.textTheme.headlineMedium!, - ); - return Scaffold( - appBar: PageWindowTitleBar( - leading: TextButton( - child: Text(context.l10n.exit), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - body: IntroductionScreen( - key: key, - globalBackgroundColor: theme.scaffoldBackgroundColor, - overrideBack: OutlinedButton( - child: Center(child: Text(context.l10n.previous)), - onPressed: () { - (key.currentState as IntroductionScreenState).previous(); - }, - ), - overrideNext: FilledButton( - child: Center(child: Text(context.l10n.next)), - onPressed: () { - (key.currentState as IntroductionScreenState).next(); - }, - ), - showBackButton: true, - overrideDone: FilledButton( - onPressed: auth.asData?.value != null - ? () { - ServiceUtils.pushNamed(context, HomePage.name); - } - : null, - child: Center(child: Text(context.l10n.done)), - ), - pages: [ - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_1, - image: Assets.tutorial.step1.image(), - bodyWidget: Wrap( - children: [ - Text(context.l10n.first_go_to), - const SizedBox(width: 5), - const Hyperlink( - "accounts.spotify.com ", - "https://accounts.spotify.com", - ), - Text(context.l10n.login_if_not_logged_in), - ], - ), - ), - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_2, - image: Assets.tutorial.step2.image(), - bodyWidget: - Text(context.l10n.step_2_steps, textAlign: TextAlign.left), - ), - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_3, - image: Assets.tutorial.step3.image(), - bodyWidget: - Text(context.l10n.step_3_steps, textAlign: TextAlign.left), - ), - if (auth.asData?.value != null) - PageViewModel( - decoration: pageDecoration.copyWith( - bodyAlignment: Alignment.center, - ), - title: context.l10n.success_emoji, - image: Assets.success.image(), - body: context.l10n.success_message, - ) - else - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_4, - bodyWidget: Column( - children: [ - Text( - context.l10n.step_4_steps, - style: theme.textTheme.labelMedium, - ), - const SizedBox(height: 10), - TokenLoginForm( - onDone: () { - GoRouter.of(context).go("/"); - }, - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index b06a67f6..1ec488c9 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,10 +9,12 @@ import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; class SettingsAccountSection extends HookConsumerWidget { @@ -23,6 +26,7 @@ class SettingsAccountSection extends HookConsumerWidget { final router = GoRouter.of(context); final auth = ref.watch(authenticationProvider); + final authNotifier = ref.watch(authenticationProvider.notifier); final scrobbler = ref.watch(scrobblerProvider); final me = ref.watch(meProvider); final meData = me.asData?.value; @@ -32,6 +36,36 @@ class SettingsAccountSection extends HookConsumerWidget { foregroundColor: Colors.white, ); + void onLogin() async { + if (kIsMobile) { + router.pushNamed(WebViewLogin.name); + return; + } + + final webview = await WebviewWindow.create(); + + webview.setOnUrlRequestCallback((url) { + final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); + + if (exp.hasMatch(url)) { + webview.getAllCookies().then((cookies) async { + final cookieHeader = + "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; + + await authNotifier.login(cookieHeader); + webview.close(); + if (context.mounted) { + context.go("/"); + } + }); + return true; + } + return false; + }); + + webview.launch("https://accounts.spotify.com/"); + } + return SectionCardWithHeading( heading: context.l10n.account, children: [ @@ -70,17 +104,11 @@ class SettingsAccountSection extends HookConsumerWidget { ), ), ), - onTap: constrains.mdAndUp - ? null - : () { - router.push("/login"); - }, + onTap: constrains.mdAndUp ? null : onLogin, trailing: constrains.smAndDown ? null : FilledButton( - onPressed: () { - router.push("/login"); - }, + onPressed: onLogin, style: ButtonStyle( shape: MaterialStateProperty.all( RoundedRectangleBorder( diff --git a/lib/provider/authentication/authentication.dart b/lib/provider/authentication/authentication.dart index 3ea8693b..08e658e8 100644 --- a/lib/provider/authentication/authentication.dart +++ b/lib/provider/authentication/authentication.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:drift/drift.dart'; @@ -160,6 +161,9 @@ class AuthenticationNotifier extends AsyncNotifier { WebStorageManager.instance().deleteAllData(); CookieManager.instance().deleteAllCookies(); } + if (kIsDesktop) { + await WebviewWindow.clearAll(); + } } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index b8e26367..2218d110 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -24,6 +25,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dart_discord_rpc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DartDiscordRpcPlugin"); dart_discord_rpc_plugin_register_with_registrar(dart_discord_rpc_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 20d4a4dd..bb0776b5 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dart_discord_rpc + desktop_webview_window file_selector_linux flutter_secure_storage_linux gtk diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 54546705..8a65bb53 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import app_links import audio_service import audio_session import bonsoir_darwin +import desktop_webview_window import device_info_plus import file_selector_macos import flutter_inappwebview_macos @@ -32,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 58d09cd9..9ab2ee38 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,8 @@ PODS: - bonsoir_darwin (0.0.1): - Flutter - FlutterMacOS + - desktop_webview_window (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -70,6 +72,7 @@ DEPENDENCIES: - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - bonsoir_darwin (from `Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin`) + - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -105,6 +108,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos bonsoir_darwin: :path: Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin + desktop_webview_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: @@ -151,6 +156,7 @@ SPEC CHECKSUMS: audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 + desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d diff --git a/pubspec.lock b/pubspec.lock index 70b0655c..14f2a5b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -474,6 +474,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + desktop_webview_window: + dependency: "direct main" + description: + path: "packages/desktop_webview_window" + ref: "feat/cookies" + resolved-ref: f20e433d4a948515b35089d40069f7dd9bced9e4 + url: "https://github.com/KRTirtho/flutter-plugins.git" + source: git + version: "0.2.4" device_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a923f5a3..a9490746 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,11 @@ dependencies: collection: ^1.15.0 curved_navigation_bar: ^1.0.3 dbus: ^0.7.8 + desktop_webview_window: + git: + url: https://github.com/KRTirtho/flutter-plugins.git + ref: feat/cookies + path: packages/desktop_webview_window device_info_plus: ^10.1.0 dio: ^5.4.3+1 disable_battery_optimization: ^1.1.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b978edb9..4fcf3019 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi")); DartDiscordRpcPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4fcc467a..d0dd6751 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links bonsoir_windows dart_discord_rpc + desktop_webview_window file_selector_windows flutter_secure_storage_windows local_notifier