PostIt.Android: drive the PKCE flow through Chrome Custom Tabs

The earlier commit removed the client_secret and wired
MainActivity.OnNewIntent to AndroidOidcCallbackSink, but
IdentityModel.OidcClient.LoginAsync still had no IBrowser to drive
the user-agent half of the flow. Without it, the desktop / browser
projects continue to fail at login with 'No browser is available'.

Android now plugs in Chrome Custom Tabs:

  * PostIt.Android/Services/AndroidSystemBrowser.cs implements
    IBrowser.InvokeAsync using CustomTabsIntent.LaunchUrl and waits
    for MainActivity.AndroidOidcCallbackSink to deliver the deep-link
    Intent (android://postit-signin?code=...&state=...).
  * PostIt/Services/Platform.cs is a tiny static indirection the
    shared library uses to ask the running platform for an
    IBrowser and the appropriate default RedirectUri, without
    referencing any UI framework from the shared assembly.
  * LoginPageViewModel reads Platform.DefaultRedirectUri and
    Platform.CreateBrowser().Invoke() before calling LoginAsync.
  * PostIt.Android/PlatformBootstrap.cs wires the Android side at
    startup, and MainActivity.OnCreate calls EnsureInitialized().
  * Xamarin.AndroidX.Browser 1.8.0 added to the central package
    versions so CustomTabsIntent resolves.
This commit is contained in:
Paul Schneider 2026-06-20 17:26:13 +01:00
commit c172d1cf9e
7 changed files with 177 additions and 6 deletions

View file

@ -0,0 +1,28 @@
using IdentityModel.OidcClient.Browser;
namespace PostIt.Services;
/// <summary>
/// Per-platform access to native integration points used by the OIDC
/// Authorization Code + PKCE flow. The shared <c>PostIt</c> library does
/// not reference any UI framework; platform projects (PostIt.Android,
/// PostIt.Desktop, PostIt.Browser) populate this class once at startup so
/// the shared <c>LoginPageViewModel</c> can drive a native browser without
/// taking a hard dependency on any specific UI toolkit.
/// </summary>
public static class Platform
{
/// <summary>
/// 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 <c>android://postit-signin</c>).
/// </summary>
public static string DefaultRedirectUri { get; set; } = "http://127.0.0.1:7890/";
/// <summary>
/// Constructs a fresh <see cref="IBrowser"/> for the running platform.
/// May return <c>null</c> if no browser is wired up; in that case
/// <c>LoginAsync</c> will surface a clear error.
/// </summary>
public static System.Func<IBrowser?>? CreateBrowser { get; set; }
}

View file

@ -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)
{