diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props
index 573989b6..4855cad3 100644
--- a/src/PostIt/Directory.Packages.props
+++ b/src/PostIt/Directory.Packages.props
@@ -13,6 +13,7 @@
+
\ No newline at end of file
diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs
index 5a75e91d..86ce394a 100644
--- a/src/PostIt/PostIt.Android/MainActivity.cs
+++ b/src/PostIt/PostIt.Android/MainActivity.cs
@@ -16,6 +16,21 @@ namespace PostIt.Android;
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity
{
+ ///
+ /// Strongly-typed handle to the current MainActivity instance, set in
+ /// and consumed by platform services such as
+ /// which need to launch
+ /// Chrome Custom Tabs.
+ ///
+ public static MainActivity? Current { get; private set; }
+
+ protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ PlatformBootstrap.EnsureInitialized();
+ Current = this;
+ }
+
///
/// Receives the deep-link Intent fired by the system browser after the
/// user completes the OIDC login on https://yavsc.pschneider.fr. The
diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs
new file mode 100644
index 00000000..56a15fb0
--- /dev/null
+++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs
@@ -0,0 +1,29 @@
+using PostIt.Services;
+using PostIt.Android.Services;
+
+namespace PostIt.Android;
+
+///
+/// One-shot platform bootstrap. Called from
+/// so that the shared
+/// LoginPageViewModel sees the Android-specific redirect URI and a
+/// working IBrowser (Chrome Custom Tabs) without referencing
+/// Android APIs from the shared library.
+///
+internal static class PlatformBootstrap
+{
+ private static int _initialized;
+
+ internal static void EnsureInitialized()
+ {
+ if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0)
+ return;
+
+ Platform.DefaultRedirectUri = Settings.AndroidRedirectUri;
+ Platform.CreateBrowser = () =>
+ {
+ var activity = MainActivity.Current;
+ return activity is null ? null : new AndroidSystemBrowser(activity);
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj
index 687f33e9..c7680ec7 100644
--- a/src/PostIt/PostIt.Android/PostIt.Android.csproj
+++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs b/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs
new file mode 100644
index 00000000..cb9c324b
--- /dev/null
+++ b/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Threading.Tasks;
+using Android.App;
+using Android.Content;
+using AndroidX.Browser.CustomTabs;
+using IdentityModel.OidcClient.Browser;
+
+namespace PostIt.Android.Services;
+
+///
+/// implementation that drives Chrome Custom Tabs for
+/// the OIDC Authorization Code + PKCE flow. The identity provider redirects
+/// to android://postit-signin?code=...&state=..., which Android
+/// routes back to the running PostIt instance via the activity-alias
+/// declared in AndroidManifest.xml; the resulting Intent URI is
+/// handed back through .
+///
+public sealed class AndroidSystemBrowser : IBrowser
+{
+ private readonly Activity _activity;
+
+ public AndroidSystemBrowser(Activity activity)
+ {
+ _activity = activity ?? throw new ArgumentNullException(nameof(activity));
+ }
+
+ public async Task InvokeAsync(BrowserOptions options, System.Threading.CancellationToken cancellationToken = default)
+ {
+ if (options is null) throw new ArgumentNullException(nameof(options));
+ if (string.IsNullOrWhiteSpace(options.StartUrl))
+ {
+ return new BrowserResult
+ {
+ ResultType = BrowserResultType.UnknownError,
+ Error = "BrowserOptions.StartUrl is empty."
+ };
+ }
+
+ var uri = global::Android.Net.Uri.Parse(options.StartUrl)!;
+
+ var callbackTask = MainActivity.AndroidOidcCallbackSink.AwaitNextCallbackAsync();
+
+ var tabsIntent = new CustomTabsIntent.Builder()
+ .SetShowTitle(true)
+ .Build();
+ tabsIntent.LaunchUrl(_activity, uri);
+
+ string responseUri;
+ try
+ {
+ responseUri = await callbackTask.WaitAsync(cancellationToken).ConfigureAwait(true);
+ }
+ catch (OperationCanceledException)
+ {
+ return new BrowserResult
+ {
+ ResultType = BrowserResultType.UserCancel
+ };
+ }
+ catch (Exception ex)
+ {
+ return new BrowserResult
+ {
+ ResultType = BrowserResultType.UnknownError,
+ Error = $"Failed to await OIDC callback: {ex.Message}"
+ };
+ }
+
+ if (string.IsNullOrEmpty(responseUri))
+ {
+ return new BrowserResult
+ {
+ ResultType = BrowserResultType.UserCancel
+ };
+ }
+
+ return new BrowserResult
+ {
+ ResultType = BrowserResultType.Success,
+ Response = responseUri
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/Services/Platform.cs b/src/PostIt/PostIt/Services/Platform.cs
new file mode 100644
index 00000000..3e552be4
--- /dev/null
+++ b/src/PostIt/PostIt/Services/Platform.cs
@@ -0,0 +1,28 @@
+using IdentityModel.OidcClient.Browser;
+
+namespace PostIt.Services;
+
+///
+/// Per-platform access to native integration points used by the OIDC
+/// Authorization Code + PKCE flow. The shared PostIt library does
+/// not reference any UI framework; platform projects (PostIt.Android,
+/// PostIt.Desktop, PostIt.Browser) populate this class once at startup so
+/// the shared LoginPageViewModel can drive a native browser without
+/// taking a hard dependency on any specific UI toolkit.
+///
+public static class Platform
+{
+ ///
+ /// Default redirect URI for the running platform. The desktop loopback
+ /// default is set here; platform projects override this property at
+ /// startup (e.g. PostIt.Android sets it to android://postit-signin).
+ ///
+ public static string DefaultRedirectUri { get; set; } = "http://127.0.0.1:7890/";
+
+ ///
+ /// Constructs a fresh for the running platform.
+ /// May return null if no browser is wired up; in that case
+ /// LoginAsync will surface a clear error.
+ ///
+ public static System.Func? CreateBrowser { get; set; }
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
index 4fb25097..82bb1833 100644
--- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.Input;
using IdentityModel.OidcClient;
+using PostIt.Services;
using System;
using System.Threading.Tasks;
@@ -21,7 +22,7 @@ public partial class LoginPageViewModel : ViewModelBase
private string _StatusMessage;
public string StatusMessage { get => _StatusMessage; private set => this.SetProperty(ref _StatusMessage, value); }
-
+
private bool _IsBusy;
public bool IsBusy { get=> _IsBusy; private set=> this.SetProperty(ref _IsBusy, value); }
@@ -37,10 +38,23 @@ public partial class LoginPageViewModel : ViewModelBase
try
{
Settings.Load().Wait();
-
- var client = new OidcClient(Settings.GetOidcClientOptions());
+
+ // The platform project picks the right redirect URI and browser
+ // implementation; we don't reference any UI toolkit from here.
+ Settings.RedirectUri = string.IsNullOrWhiteSpace(Settings.RedirectUri)
+ ? Platform.DefaultRedirectUri
+ : Settings.RedirectUri;
+
+ var browser = Platform.CreateBrowser?.Invoke();
+ if (browser is null)
+ {
+ StatusMessage = "No browser is available on this platform.";
+ return;
+ }
+
+ var client = new OidcClient(Settings.GetOidcClientOptions(browser));
var loginResult = await client.LoginAsync(new LoginRequest());
-
+
if (loginResult.IsError)
{
StatusMessage = loginResult.Error;
@@ -51,12 +65,12 @@ public partial class LoginPageViewModel : ViewModelBase
this.IsBusy = false;
AccessToken = loginResult.AccessToken;
- /* TODO save or not user log and password
+ /* TODO save or not user log and password
await Task.Run(() =>
{
Settings.Save().Wait();
});*/
-
+
}
catch (Exception ex)
{