From 52769238bd2816fe7c26c6d7364a73342f54a049 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sun, 19 Jul 2026 22:46:39 -0400 Subject: [PATCH] oauth support is almost done --- android/app/src/main/AndroidManifest.xml | 8 ++++ lib/controllers/auth_url.dart | 2 +- lib/controllers/client.dart | 4 ++ lib/controllers/client_id.dart | 4 +- lib/main.dart | 1 + lib/models/requests/oauth/exchange_token.dart | 17 ++++++++ lib/pages/chat.dart | 1 + lib/pages/select_server.dart | 30 +++++++++++++- linux/flutter/generated_plugin_registrant.cc | 4 ++ linux/flutter/generated_plugins.cmake | 1 + linux/nexus.federated.Nexus.desktop | 5 ++- linux/runner/my_application.cc | 11 ++++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Runner/Info.plist | 13 ++++++ pubspec.lock | 40 +++++++++++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + windows/runner/main.cpp | 7 +++- 19 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 lib/models/requests/oauth/exchange_token.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 666977e..a0a0bcc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,14 @@ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> + + + + + + + + { .getAuthUrl( .new( homeserverUrl: homeserver, - redirectUri: .new(scheme: "nexus.federated.nexus"), + redirectUri: .new(scheme: "nexus.federated.nexus", path: "/"), responseMode: .query, scopes: .new([.clientApi]), clientId: await ref.watch( diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index 28c4d08..cd67a62 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -22,6 +22,7 @@ import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/join_room.dart"; import "package:nexus/models/profile.dart"; +import "package:nexus/models/requests/oauth/exchange_token.dart"; import "package:nexus/models/requests/oauth/get_auth_url.dart"; import "package:nexus/models/requests/oauth/register_client.dart"; import "package:nexus/models/requests/paginate.dart"; @@ -288,6 +289,9 @@ class ClientController extends AsyncNotifier { await _sendCommand("oauth_get_authorization_url", request.toJson()), ); + Future exchangeToken(OAuthExchangeTokenRequest request) async => + await _sendCommand("oauth_exchange_token", request.toJson()); + Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { diff --git a/lib/controllers/client_id.dart b/lib/controllers/client_id.dart index 8be30f2..ac8f5cb 100644 --- a/lib/controllers/client_id.dart +++ b/lib/controllers/client_id.dart @@ -20,7 +20,9 @@ class ClientIdController extends AsyncNotifier { ), homeserverUrl: homeserver, clientUri: Uri.https("nexus.federated.nexus"), - redirectUris: .new([.new(scheme: "nexus.federated.nexus")]), + redirectUris: .new([ + .new(scheme: "nexus.federated.nexus", path: "/"), + ]), ), ); diff --git a/lib/main.dart b/lib/main.dart index df3ed91..05731e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -79,6 +79,7 @@ void main() async { runApp( ProviderScope( + retry: null, observers: [ // Change false to true if you want debug information on provider reloads // ignore: dead_code diff --git a/lib/models/requests/oauth/exchange_token.dart b/lib/models/requests/oauth/exchange_token.dart new file mode 100644 index 0000000..9d55c1f --- /dev/null +++ b/lib/models/requests/oauth/exchange_token.dart @@ -0,0 +1,17 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "exchange_token.freezed.dart"; +part "exchange_token.g.dart"; + +@freezed +abstract class OAuthExchangeTokenRequest with _$OAuthExchangeTokenRequest { + const factory OAuthExchangeTokenRequest({ + required Uri homeserverUrl, + required String codeVerifier, + required Uri redirectUri, + required String code, + required String clientId, + }) = _OAuthExchangeTokenRequest; + + factory OAuthExchangeTokenRequest.fromJson(Map json) => + _$OAuthExchangeTokenRequestFromJson(json); +} diff --git a/lib/pages/chat.dart b/lib/pages/chat.dart index 235aecc..fdd3763 100644 --- a/lib/pages/chat.dart +++ b/lib/pages/chat.dart @@ -18,6 +18,7 @@ class ChatPage extends ConsumerWidget { final showMembersByDefault = constraints.maxWidth > 1000; final initComplete = ref.watch(InitCompleteController.provider); final roomId = ref.watch(KeyController.provider(KeyController.roomKey)); + ref.read(EmojiController.provider); return Scaffold( diff --git a/lib/pages/select_server.dart b/lib/pages/select_server.dart index 2d02e28..b4cca9c 100644 --- a/lib/pages/select_server.dart +++ b/lib/pages/select_server.dart @@ -1,9 +1,11 @@ +import "package:app_links/app_links.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_svg/flutter_svg.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/auth_url.dart"; import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/client_id.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/main.dart"; import "package:nexus/models/homeserver.dart"; @@ -48,11 +50,35 @@ class SelectServerPage extends HookConsumerWidget { ), ); } else { - final authUrl = await ref.read( + final codeResponse = await ref.watch( AuthUrlController.provider(newUrl).future, ); - await ref.watch(LaunchHelper.provider).launchUrl(authUrl.url); + await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); + + AppLinks().uriLinkStream.listen((encodedUri) async { + final code = encodedUri.queryParameters["code"]; + + if (code != null) { + await ref + .watch(ClientController.provider.notifier) + .exchangeToken( + .new( + homeserverUrl: newUrl, + codeVerifier: codeResponse.codeVerifier, + redirectUri: .new( + scheme: "nexus.federated.nexus", + path: "/", + ), + code: code, + clientId: await ref.watch( + ClientIdController.provider(newUrl).future, + ), + ), + ) + .onError(showError); + } + }); // await Navigator.of(context).push( // MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)), // ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 755a54e..603ea6a 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { 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); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b9ca03c..7d8f046 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color file_selector_linux + gtk media_kit_libs_linux media_kit_video screen_retriever_linux diff --git a/linux/nexus.federated.Nexus.desktop b/linux/nexus.federated.Nexus.desktop index d3fa575..d61734e 100644 --- a/linux/nexus.federated.Nexus.desktop +++ b/linux/nexus.federated.Nexus.desktop @@ -2,8 +2,9 @@ Name=Nexus GenericName=Matrix Client Comment=A simple and user-friendly Matrix client -Exec=nexus +Exec=nexus %u Icon=nexus Terminal=false Type=Application -Categories=Chat;Network;InstantMessaging; \ No newline at end of file +Categories=Chat;Network;InstantMessaging; +MimeType=x-scheme-handler/nexus.federated.nexus; \ No newline at end of file diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index abf5dc5..62e5450 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -23,6 +23,13 @@ static void first_frame_cb(MyApplication* self, FlView *view) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -93,7 +100,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GApplication::startup. @@ -140,6 +147,6 @@ MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c1b7cee..7b5a69f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import dynamic_color import file_picker import file_selector_macos @@ -18,6 +19,7 @@ import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..36999ec 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -28,5 +28,18 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleURLName + + auth_callback + CFBundleURLSchemes + + + nexus.federated.nexus + + + diff --git a/pubspec.lock b/pubspec.lock index 9c978d3..4813bb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.11" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -558,6 +590,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" hooks: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 98dcf40..33f0ffd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ dependencies: m3e_card_list: ^0.1.0 xdg_directories: ^1.1.0 package_info_plus: ^9.0.1 + app_links: ^7.0.0 dev_dependencies: build_runner: 2.15.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7938787..0694802 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -15,6 +16,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 64ef5b5..238ef93 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links dynamic_color file_selector_windows media_kit_libs_windows_video diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 453f9cf..590e663 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -1,12 +1,17 @@ #include #include #include - +#include "app_links/app_links_plugin_c_api.h" #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // Forward the link to an existing instance, if any, then exit. + // You may ignore the result if you need to create another window. + if (SendAppLinkToInstance()) { + return EXIT_SUCCESS; + } // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {