diff --git a/.vscode/launch.json b/.vscode/launch.json index 36dc17a2..dc8d3c68 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,19 +5,19 @@ "version": "0.2.0", "configurations": [ { - "name": "Android", + "name": "Android Debug", "type": "mono", "preLaunchTask": "run-debug-android", "request": "attach", "address": "localhost", - "port": 10000 + "port": 55555 }, { - "name": "Attach - Android", + "name": "Android Attach - Debug", "type": "mono", "request": "attach", "address": "localhost", - "port": 10000 + "port": 55555 }, { "name": "API", @@ -26,19 +26,19 @@ "projectPath": "${workspaceFolder}/src/Api/Api.csproj" }, { - "name": "Yavsc.Org", + "name": "Yavsc Org", "type": "dotnet", "request": "launch", "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", }, { - "name": "Yavsc.Blogs", + "name": "Yavsc Blogs", "type": "dotnet", "request": "launch", "projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj" }, { - "name": "PostIt", + "name": "PostIt Desktop", "type": "dotnet", "request": "launch", "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj", diff --git a/.vscode/settings.json b/.vscode/settings.json index 16bbe483..915683fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "appsettings", "asciidoctor", "ASPNETCORE", + "Avalonia", "Configurabilité", "Cratie", "DESTDIR", @@ -15,6 +16,7 @@ "Hsts", "Newtonsoft", "Npgsql", + "Oidc", "PKCE", "postit", "pschneider", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9fe9caaf..3c0c0bf2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,8 +18,8 @@ "-p:TargetFramework=net10.0-android", "-p:Configuration=Debug", "-p:AndroidAttachDebugger=true", - "-p:AndroidSdbHostPort=10000", - "-p:AndroidSdbTargetPort=10000" + "-p:AndroidSdbHostPort=55555", + "-p:AndroidSdbTargetPort=55555" ], "problemMatcher": [ { diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 4f788550..ad8455ef 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -1,13 +1,11 @@ -using Android; + using Android.App; using Android.Content; using Android.Content.PM; using AndroidX.Core.Provider; using AndroidX.Emoji2.Text; -using Avalonia; using Avalonia.Android; -using AndroidX.Core.Provider; -using AndroidX.Emoji2.Text; +using PostIt.Droid.Services; namespace PostIt.Android; @@ -34,7 +32,7 @@ public class MainActivity : AvaloniaMainActivity Yavsc.Resource.Array.com_google_android_gms_fonts_certs); //com_google_android_gms_fonts_certs EmojiCompat.Config config = new FontRequestEmojiCompatConfig(this, fontRequest); EmojiCompat.Init(config); - PlatformBootstrap.EnsureInitialized(); + PlatformBootstrap.InitPlatform(); base.OnCreate(savedInstanceState); Current = this; } @@ -50,7 +48,13 @@ public class MainActivity : AvaloniaMainActivity protected override void OnNewIntent(Intent? intent) { base.OnNewIntent(intent); - if (intent is not null) AndroidOidcCallbackSink.Handle(intent); + + var url = intent?.DataString; + if (!string.IsNullOrEmpty(url) && url.StartsWith("postit://callback")) + { + OidcCallbackManager.SetResult(url); + } + } internal static class AndroidOidcCallbackSink diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index d59b154f..f208f9ce 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -12,14 +12,9 @@ namespace PostIt.Android; /// internal static class PlatformBootstrap { - private static int _initialized; - - internal static void EnsureInitialized() + internal static void InitPlatform() { - if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0) - return; - Platform.DefaultRedirectUri = ViewModels.Settings.AndroidRedirectUri; Platform.CreateBrowser = () => { var activity = MainActivity.Current; diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml index 91b61d05..8793aae8 100644 --- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml +++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml @@ -2,31 +2,5 @@ - - - - - - - - - - diff --git a/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs b/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs index b6716e22..bb10b364 100644 --- a/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs +++ b/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Android.App; using AndroidX.Browser.CustomTabs; using IdentityModel.OidcClient.Browser; +using PostIt.Droid.Services; namespace PostIt.Android.Services; @@ -35,9 +36,14 @@ public sealed class AndroidSystemBrowser : IBrowser }; } - var uri = global::Android.Net.Uri.Parse(options.StartUrl)!; + // 1. Enregistrez la tâche avant de lancer le Custom Tab + var callbackTask = OidcCallbackManager.RegisterCallback(cancellationToken); - var callbackTask = MainActivity.AndroidOidcCallbackSink.AwaitNextCallbackAsync(); + // 2. LANCEZ VOTRE CUSTOM TAB ICI (via AndroidX.Browser.CustomTabs) + // ... code pour ouvrir l'URL d'authentification ... + + + var uri = global::Android.Net.Uri.Parse(options.StartUrl)!; var tabsIntent = new CustomTabsIntent.Builder() .SetShowTitle(true)! diff --git a/src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs b/src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs new file mode 100644 index 00000000..30f8fa18 --- /dev/null +++ b/src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PostIt.Droid.Services; + +public static class OidcCallbackManager +{ + private static TaskCompletionSource? _tcs; + + public static Task RegisterCallback(CancellationToken cancellationToken) + { + _tcs = new TaskCompletionSource(); + cancellationToken.Register(() => _tcs.TrySetCanceled()); + return _tcs.Task; + } + + public static void SetResult(string url) + { + _tcs?.TrySetResult(url); + } +} diff --git a/src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs b/src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs new file mode 100644 index 00000000..9ed2eb18 --- /dev/null +++ b/src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs @@ -0,0 +1,35 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using PostIt.Droid.Services; + +namespace PostIt.Android; + +[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)] +[IntentFilter(new[] { Intent.ActionView }, + Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, + DataScheme = "postit", // Remplacez par votre schéma personnalisé (ex: yavsc ou postit) + DataHost = "callback")] // Correspond à postit://callback +public class WebAuthenticationCallbackActivity : Activity +{ + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + // Capturer l'URL de redirection OIDC + var url = Intent?.DataString; + + if (!string.IsNullOrEmpty(url)) + { + // Transmettre l'URL au gestionnaire partagé pour compléter la Task + OidcCallbackManager.SetResult(url); + } + + // Fermer cette activité transparente et ramener l'application au premier plan + var intent = new Intent(this, typeof(MainActivity)); + intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop); + StartActivity(intent); + Finish(); + } +} diff --git a/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs b/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs deleted file mode 100644 index ff9ca7f6..00000000 --- a/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs +++ /dev/null @@ -1,29 +0,0 @@ -using PostIt.Services; - -namespace PostIt.Desktop; - -/// -/// One-shot platform bootstrap. Called from Program.Main so that -/// the shared OIDC login path sees a working IBrowser — the -/// custom-scheme browser that hands the OIDC callback off to the -/// running instance through the named pipe. Desktop builds do NOT use -/// a loopback HTTP listener: the postit:// scheme is registered -/// with the OS at install time and the browser is whatever the user -/// has configured to open it. -/// -internal static class PlatformBootstrap -{ - private static int _initialized; - - internal static void EnsureInitialized() - { - if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0) - return; - - // Use the custom-scheme redirect on Desktop. Loopback is only - // a fallback for platforms that cannot register postit:// - // (see Settings.DefaultLoopbackRedirectUri for that path). - Platform.DefaultRedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri; - Platform.CustomScheme = "postit"; - } -} diff --git a/src/PostIt/PostIt.Desktop/Program.cs b/src/PostIt/PostIt.Desktop/Program.cs index f22f79b0..0de3bd69 100644 --- a/src/PostIt/PostIt.Desktop/Program.cs +++ b/src/PostIt/PostIt.Desktop/Program.cs @@ -12,8 +12,6 @@ sealed class Program [STAThread] public static void Main(string[] args) { - PlatformBootstrap.EnsureInitialized(); - // Short-circuit 2nd-instance launches (OS handing us the // postit://callback URL) BEFORE Avalonia spins up a window. // If we let Avalonia initialise, the new MainWindow flashes diff --git a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs index dc58e9fc..43021caa 100644 --- a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs +++ b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs @@ -85,7 +85,7 @@ public class SettingsLoadTests bool flip = ((workerId + i) & 1) == 0; settings.DarkMode = flip; settings.Authentication.RedirectUri = - global::AuthenticationSettings.DefaultDesktopRedirectUri; + global::AuthenticationSettings.DesktopRedirectUri; settings.BusinessApiUrl = flip ? "https://a.example.test/api/v1/" diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index f8430347..1b019f6e 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; @@ -73,10 +72,8 @@ public partial class App : Application ConfigureRootView(View); ApplyDarkMode(settings); } - var mainVm = ServiceProvider!.GetRequiredService(); base.OnFrameworkInitializationCompleted(); - this.PushPageAsync(mainVm); } private void ConfigureRootView(MainView rootView) @@ -93,9 +90,9 @@ private void ConfigureRootView(MainView rootView) rootView.NavRoot.PopToRootAsync(); }; - sessionStatus.LoginSucceeded += () => + sessionStatus.LoginSucceeded += async () => { - PushMainPageAsync(); + await PushMainPageAsync(); }; rootView.SessionBanner.DataContext = sessionStatus; @@ -133,6 +130,9 @@ private void ConfigureRootView(MainView rootView) var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true); var sessionStatus = provider.GetRequiredService(); sessionStatus.Refresh(); + var homePage = provider.GetRequiredService(); + var app = (App)Current!; + await app.PushPageAsync(homePage); if (!refreshed) return; await PushMainPageAsync().ConfigureAwait(true); diff --git a/src/PostIt/PostIt/Services/Platform.cs b/src/PostIt/PostIt/Services/Platform.cs index c867c63c..8e5f7e25 100644 --- a/src/PostIt/PostIt/Services/Platform.cs +++ b/src/PostIt/PostIt/Services/Platform.cs @@ -21,14 +21,14 @@ public static class Platform /// override this property at startup (e.g. PostIt.Android sets /// it to android://postit-signin). /// - public static string DefaultRedirectUri { get; set; } = "postit://callback"; + public const string RedirectUri = "postit://callback"; /// /// Scheme prefix the matches /// against BrowserOptions.EndUrl. Overridable for apps /// that want to register their own scheme. /// - public static string CustomScheme { get; set; } = "postit"; + public const string CustomScheme = "postit"; /// /// Constructs a fresh for the running platform. @@ -37,4 +37,4 @@ public static class Platform /// public static System.Func? CreateBrowser { get; set; } = () => new CustomSchemeBrowser(CustomScheme); -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs index 99358cfb..9ebeaebd 100644 --- a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs +++ b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs @@ -10,7 +10,7 @@ public partial class AuthenticationSettings : ObservableObject /// hand-off in /// (RFC 8252 §7.1). Production Desktop builds use this. /// - public const string DefaultDesktopRedirectUri = "postit://callback"; + public const string DesktopRedirectUri = "postit://callback"; /// /// Redirect URI used by the Android app. The corresponding IntentFilter @@ -34,15 +34,19 @@ public partial class AuthenticationSettings : ObservableObject [ObservableProperty] public partial string[] Scopes { get; set; } - /// - /// OAuth redirect URI. Defaults to + /// OAuth redirect URI. Defaults to /// (custom URI scheme) which is the right answer for desktop /// production builds. Mobile platforms must set this to /// before calling LoginAsync. /// [ObservableProperty] - public partial string RedirectUri { get; set; } = DefaultDesktopRedirectUri; + public partial string RedirectUri { get; set; } +#if ANDROID + = AndroidRedirectUri; +#else + = DesktopRedirectUri; +#endif /// /// Space-separated view of . Exists for the diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 2115a0cd..fd87d772 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -2,13 +2,11 @@ using System.Runtime.CompilerServices; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using IdentityModel.OidcClient; -using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text.Json; -using System.Threading; [assembly: InternalsVisibleTo("PostIt.Tests")] @@ -18,37 +16,6 @@ public partial class Settings : ViewModelBase { const string SettingsFileName = "postit-settings.json"; - /// - /// Redirect URI used by the Android app. The corresponding IntentFilter - /// in PostIt.Android/Properties/AndroidManifest.xml must match. - /// - public const string AndroidRedirectUri = "android://postit-signin"; - - - - /// - /// Process-wide canonical instance, wired up - /// at application boot by - /// through . The hybrid pattern: - /// - /// The static Current reference gives - /// ViewModels a non-DI way to reach the same instance (and lets - /// the framework bindings push notifications through one stable - /// ). - /// Tests that want to exercise a clean - /// instance still call new Settings(); Current - /// stays null in those contexts because - /// is never invoked. - /// Reads () are - /// thread-safe and never allocate; mutations always go through - /// the DI-resolved singleton so two threads cannot each register - /// a different "current" Settings. - /// - /// - private static Settings? s_current; - - - [ObservableProperty] public partial AuthenticationSettings Authentication { get; set; } = new(); @@ -66,7 +33,7 @@ public partial class Settings : ViewModelBase /// setters above all funnel through here, and we flip /// in lock-step. Sub-property mutations /// (e.g. Authentication.Authority) are caught by the - /// subscription wired up in + /// subscription wired up in /// below. disables the flag during bulk /// hydration so the disk load itself does not count as a user /// edit. @@ -336,7 +303,7 @@ public partial class Settings : ViewModelBase this.Authentication.ClientId = string.IsNullOrWhiteSpace(settings.Authentication.ClientId) ? AuthenticationSettings.DefaultClientId : settings.Authentication.ClientId; this.Authentication.RedirectUri = string.IsNullOrWhiteSpace(settings.Authentication.RedirectUri) ? - AuthenticationSettings.DefaultDesktopRedirectUri : settings.Authentication.RedirectUri; + AuthenticationSettings.DesktopRedirectUri : settings.Authentication.RedirectUri; if (settings.Authentication.Scopes is null || settings.Authentication.Scopes.Length == 0) { settings.Authentication.Scopes = AuthenticationSettings.DefaultScopes; @@ -377,7 +344,7 @@ public partial class Settings : ViewModelBase { Authority = AuthenticationSettings.DefaultAuthority, ClientId = AuthenticationSettings.DefaultClientId, - RedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri, + RedirectUri = AuthenticationSettings.DesktopRedirectUri, Scopes = AuthenticationSettings.DefaultScopes }; this.DarkMode = false;