yavsc/src/PostIt/PostIt.Android/MainActivity.cs
Paul Schneider c172d1cf9e 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.
2026-06-20 17:26:13 +01:00

66 lines
No EOL
2.4 KiB
C#

using Android.App;
using Android.Content.PM;
using Android.Content;
using Avalonia;
using Avalonia.Android;
namespace PostIt.Android;
[Activity(
Name = "PostIt.Android.PostItMainActivity",
Label = "PostIt.Android",
Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTask,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity
{
/// <summary>
/// Strongly-typed handle to the current MainActivity instance, set in
/// <see cref="OnCreate"/> and consumed by platform services such as
/// <see cref="Services.AndroidSystemBrowser"/> which need to launch
/// Chrome Custom Tabs.
/// </summary>
public static MainActivity? Current { get; private set; }
protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
PlatformBootstrap.EnsureInitialized();
Current = this;
}
/// <summary>
/// Receives the deep-link Intent fired by the system browser after the
/// user completes the OIDC login on https://yavsc.pschneider.fr. The
/// Intent URI has the shape <c>android://postit-signin?code=...&amp;state=...</c>.
///
/// IdentityModel.OidcClient.Browser.SystemBrowser is set up to await this
/// callback via a TaskCompletionSource; expose the received Intent here
/// through a static sink so the browser can resolve the pending login.
/// </summary>
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
if (intent is not null) AndroidOidcCallbackSink.Handle(intent);
}
internal static class AndroidOidcCallbackSink
{
private static System.Threading.Tasks.TaskCompletionSource<string>? _pending;
public static System.Threading.Tasks.Task<string> AwaitNextCallbackAsync()
{
_pending = new System.Threading.Tasks.TaskCompletionSource<string>(
System.Threading.Tasks.TaskCreationOptions.RunContinuationsAsynchronously);
return _pending.Task;
}
public static void Handle(Intent intent)
{
var tcs = System.Threading.Interlocked.Exchange(ref _pending, null);
tcs?.TrySetResult(intent?.Data?.ToString() ?? string.Empty);
}
}
}