diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs
index 56a15fb0..0d11035a 100644
--- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs
+++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs
@@ -5,10 +5,10 @@ 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.
+/// so that the shared OIDC login
+/// path 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
{
diff --git a/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs b/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs
index e93480fb..ad283ec1 100644
--- a/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs
+++ b/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs
@@ -5,8 +5,8 @@ namespace PostIt.Desktop;
///
/// One-shot platform bootstrap. Called from Program.Main so that
-/// the shared LoginPageViewModel sees a working IBrowser
-/// — the custom-scheme browser that hands the OIDC callback off to the
+/// 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
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index 134ae42f..d9938f72 100644
--- a/src/PostIt/PostIt/App.axaml.cs
+++ b/src/PostIt/PostIt/App.axaml.cs
@@ -60,7 +60,6 @@ public partial class App : Application
// Vues
services.AddTransient();
- services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -71,7 +70,6 @@ public partial class App : Application
services.AddSingleton(client);
services.AddTransient();
services.AddTransient();
- services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -86,10 +84,9 @@ public partial class App : Application
// Bind the canonical Settings to the static accessor so any
// code path that can't easily take a constructor parameter
- // (designer surfaces, Avalonia data templates, the
- // LoginPage.axaml.cs fallback) still gets the same instance
- // the rest of the app is using. Idempotent: re-binding from
- // a second App boot (tests) is a no-op.
+ // (designer surfaces, Avalonia data templates) still gets
+ // the same instance the rest of the app is using. Idempotent:
+ // re-binding from a second App boot (tests) is a no-op.
Settings.BindToServiceProvider(provider);
Services = provider;
@@ -125,6 +122,14 @@ public partial class App : Application
_ = nav.PopToRootAsync();
};
+ // When the user signs in interactively (Login button on
+ // the session banner), push MainPage on top of HomePage.
+ sessionStatus.LoginSucceeded += () =>
+ {
+ var w = (MainWindow)((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime!).MainWindow!;
+ _ = PushMainPageAsync(provider, w);
+ };
+
window.Opened += async (_, _) => await BootAsync(provider, api, window);
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
@@ -154,10 +159,22 @@ public partial class App : Application
sessionStatus.Refresh();
if (!refreshed) return;
+ await PushMainPageAsync(provider, window).ConfigureAwait(true);
+ }
+
+ ///
+ /// Resolve a fresh MainPage + VM from DI and push it on top
+ /// of the current navigation stack. Used both by
+ /// (silent refresh at boot) and by SessionStatusViewModel.LoginSucceeded
+ /// (interactive login from the banner). Pulled out as a helper so
+ /// the two callers can't drift apart.
+ ///
+ private static async Task PushMainPageAsync(IServiceProvider provider, MainWindow window)
+ {
var mainVm = provider.GetRequiredService();
var mainPage = provider.GetRequiredService();
mainPage.DataContext = mainVm;
- await window.NavRoot.PushAsync(mainPage);
+ await window.NavRoot.PushAsync(mainPage).ConfigureAwait(true);
}
private bool TryHandOffCustomSchemeUrl()
diff --git a/src/PostIt/PostIt/Services/Platform.cs b/src/PostIt/PostIt/Services/Platform.cs
index 258cd5b5..c867c63c 100644
--- a/src/PostIt/PostIt/Services/Platform.cs
+++ b/src/PostIt/PostIt/Services/Platform.cs
@@ -7,8 +7,8 @@ namespace PostIt.Services;
/// 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.
+/// the shared OIDC login path can drive a native browser without taking
+/// a hard dependency on any specific UI toolkit.
///
public static class Platform
{
diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs
index 0a42773b..1fe02e86 100644
--- a/src/PostIt/PostIt/Services/YavscApiClient.cs
+++ b/src/PostIt/PostIt/Services/YavscApiClient.cs
@@ -57,7 +57,7 @@ public class YavscApiClient : IAsyncDisposable
///
/// True if a non-expired access token (or a refreshable bundle) is
- /// already in memory. UI uses this to skip the LoginPage on warm
+ /// already in memory. UI uses this to skip the login flow on warm
/// starts.
///
public bool HasValidSession
@@ -74,9 +74,9 @@ public class YavscApiClient : IAsyncDisposable
///
/// The current access token, or null if no session is active.
- /// Surfaced so the LoginPageViewModel can mirror it onto its own
- /// observable property (and so the OIDC id_token / claims can be
- /// shown in the UI).
+ /// Surfaced so consumers (e.g. HomePage) can mirror it onto
+ /// their own observable properties and so the OIDC id_token / claims
+ /// can be shown in the UI.
///
public string? CurrentAccessToken => _tokens?.AccessToken;
@@ -87,9 +87,7 @@ public class YavscApiClient : IAsyncDisposable
/// Optional sink for the discrete phases of
/// the flow; the UI uses this to render a debug-friendly status
/// (Discovering → OpeningBrowser → AwaitingCallback → ExchangingCode
- /// → Success / Error). The same caller can also rely on
- /// for the human
- /// text (URLs, error detail).
+ /// → Success / Error).
public async Task LoginInteractiveAsync(
IProgress? progress = null,
CancellationToken ct = default)
diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs
index e6d0e91a..ba34bf7c 100644
--- a/src/PostIt/PostIt/ViewLocator.cs
+++ b/src/PostIt/PostIt/ViewLocator.cs
@@ -27,7 +27,6 @@ public class ViewLocator : IDataTemplate
{
MainPageViewModel => _services.GetRequiredService(),
SettingsPageViewModel => _services.GetRequiredService(),
- LoginPageViewModel => _services.GetRequiredService(),
HomePageViewModel => _services.GetRequiredService(),
SignaturePageViewModel => _services.GetRequiredService(),
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
deleted file mode 100644
index a6ed595d..00000000
--- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
+++ /dev/null
@@ -1,341 +0,0 @@
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.Input;
-using IdentityModel.OidcClient.Browser;
-using PostIt.Services;
-
-namespace PostIt.ViewModels;
-
-public partial class LoginPageViewModel : ViewModelBase
-{
- private const string SettingsFileName = "postit-settings.json";
-
- [Obsolete("Password grant is not used; IdentityModel.OidcClient performs PKCE.")]
- public string Password { get; set; } = string.Empty;
-
- [Obsolete("User-entered email is not used; the IdP login UI collects it.")]
- public string UserEmail { get; set; } = string.Empty;
-
- [Obsolete("No local credential persistence in the current build.")]
- public bool RememberMe { get; set; }
-
- ///
- /// URL of the Yavsc.Org account-registration page.
- /// Derived from 's Authority.
- /// Empty when the authority is not configured.
- ///
- public string RegisterUrl =>
- BuildExternalUrl("/Account/Register");
-
- ///
- /// URL of the Yavsc.Org password-reset page (open to anonymous users).
- /// Derived from 's Authority.
- /// Empty when the authority is not configured.
- ///
- public string ForgotPasswordUrl =>
- BuildExternalUrl("/Account/ForgotPassword");
-
- public bool HasRegisterUrl => !string.IsNullOrEmpty(RegisterUrl);
- public bool HasForgotPasswordUrl => !string.IsNullOrEmpty(ForgotPasswordUrl);
-
- ///
- /// Canonical authority with any trailing
- /// slash removed. Used as the base for both the OIDC discovery URL and the
- /// human-facing Account URLs (Register / Forgot password). Empty when the
- /// authority is not configured.
- ///
- public string ExternalUrl => BuildExternalUrl(string.Empty);
-
- ///
- /// OIDC discovery URL the client actually calls during login:
- /// ExternalUrl + "/.well-known/openid-configuration". Surfaced in
- /// on failure so the operator can copy it
- /// verbatim and verify reachability from a browser.
- ///
- public string DiscoveryUrl =>
- string.IsNullOrEmpty(ExternalUrl) ? string.Empty : ExternalUrl + "/.well-known/openid-configuration";
-
- ///
- /// True when the settings file is missing or Authentication.Authority
- /// is empty. The LoginPage surfaces a banner in that case and disables
- /// the Register / Forgot password buttons.
- ///
- public bool ConfigMissing =>
- string.IsNullOrWhiteSpace(Settings.Authentication?.Authority);
-
- ///
- /// Localised banner shown when is true.
- /// The path follows the XDG spec on Linux (where PostIt.Desktop runs):
- /// the file is expected at ~/.config/PostIt/postit-settings.json.
- ///
- public string ConfigMissingMessage =>
- $"Configuration PostIt manquante — voir ~/.config/PostIt/postit-settings.json";
-
- private string BuildExternalUrl(string path)
- {
- var authority = Settings.Authentication?.Authority?.TrimEnd('/');
- return string.IsNullOrEmpty(authority)
- ? string.Empty
- : authority + path;
- }
-
- ///
- /// The access token of the most recent successful login, or null.
- /// Kept on the VM so views can show "logged in as …" feedback; the
- /// authoritative copy lives in the .
- ///
- private string? _accessToken;
- public string? AccessToken
- {
- get => _accessToken;
- private set => this.SetProperty(ref _accessToken, value);
- }
-
- public override bool CanNavigateNext { get => false; protected set => throw new NotImplementedException(); }
- public override bool CanNavigatePrevious { get => true; protected set => throw new NotImplementedException(); }
-
- public Settings Settings { get; }
-
- ///
- /// Discrete phase of the OIDC flow the LoginPage is currently
- /// showing. Surfaced in the UI as a one-line status (Discovering /
- /// OpeningBrowser / AwaitingCallback / ExchangingCode / Success /
- /// Error). Operators use this to debug the custom-scheme
- /// callback hand-off: when AwaitingCallback never resolves,
- /// the OS never re-launched PostIt with the postit:// URL.
- ///
- private OIDCLoginPhase _phase = OIDCLoginPhase.Idle;
- public OIDCLoginPhase Phase
- {
- get => _phase;
- private set
- {
- if (this.SetProperty(ref _phase, value))
- OnPropertyChanged(nameof(PhaseLabel));
- }
- }
-
- ///
- /// Human-readable label for . French to match
- /// the rest of the UI. Computed once per phase change.
- ///
- public string PhaseLabel => _phase switch
- {
- OIDCLoginPhase.Idle => "En attente",
- OIDCLoginPhase.Discovering => "Découverte OIDC…",
- OIDCLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
- OIDCLoginPhase.AwaitingCallback => "En attente du callback postit://…",
- OIDCLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
- OIDCLoginPhase.Success => "Connecté",
- OIDCLoginPhase.Error => "Erreur",
- _ => _phase.ToString(),
- };
-
- private string _statusMessage = "Ready";
- 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);
- }
-
- private bool _LoginSuccess;
- public bool LoginSuccess { get => _isBusy;
- private set => this.SetProperty(ref _LoginSuccess, value); }
-
- ///
- /// Optional override used by tests. When set, this factory is called
- /// instead of to obtain the
- /// instance.
- ///
- public Func? BrowserFactoryOverride { get; set; }
-
- ///
- /// Optional override used by tests. When set, this delegate replaces
- /// the call to at the start of
- /// , so tests can inject a Settings object
- /// without it being overwritten by the user/embedded default.
- ///
- public Func? SettingsLoadOverride { get; set; }
-
- ///
- /// Optional override used by tests. When set, the VM hands this
- /// pre-built to itself instead of
- /// constructing a fresh one.
- ///
- public YavscApiClient? ApiClientOverride { get; set; }
- public Action LoginSucceeded { get; internal set; }
-
- private YavscApiClient? _api;
-
- ///
- /// Designer / Avalonia-data-template fallback. Resolves the
- /// canonical Settings singleton through the running App's DI
- /// container. Throws when called outside a bound App (e.g. a
- /// stray unit test instantiating the VM directly) so we cannot
- /// silently end up with a second Settings instance racing the
- /// singleton at runtime — that race is the exact bug that
- /// crashed postit://callback re-launches. Tests that
- /// don't want the DI bind pass an explicit Settings to
- /// the parameterised constructor. The cross-thread crash is
- /// also fixed at the Settings layer (thread-safe PropertyChanged
- /// marshalling) so the duplicate-instance race is now caught
- /// loudly instead of corrupting Avalonia state.
- ///
- public LoginPageViewModel() : this(Settings.RequireCurrent(), apiClient: null, browserFactoryOverride: null)
- {
- // Load settings eagerly so RegisterUrl / ForgotPasswordUrl are
- // populated as soon as the page renders (XAML bindings fire
- // before the user clicks Login). Settings.Load is synchronous
- // on purpose; calling .GetAwaiter().GetResult() on it would
- // deadlock the UI thread on the await inside the file read.
- try { Settings.Load(); }
- catch { /* settings may be missing in tests/dev; LoginAsync will surface real errors */ }
- }
-
- ///
- /// Test-friendly constructor: caller supplies pre-loaded
- /// , an optional
- /// that bypasses the
- /// static indirection, and an optional
- /// pre-built for end-to-end
- /// scenarios where the test owns the wiring.
- ///
- public LoginPageViewModel(
- Settings settings,
- Func? browserFactoryOverride = null,
- YavscApiClient? apiClient = null)
- {
- Settings = settings;
- BrowserFactoryOverride = browserFactoryOverride;
- ApiClientOverride = apiClient;
- StatusMessage = "Ready";
- }
-
- [RelayCommand]
- public async Task LoginAsync()
- {
- try
- {
- IsBusy = true;
- LoginSuccess = false;
- if (SettingsLoadOverride is not null)
- await SettingsLoadOverride().ConfigureAwait(false);
- else
- Settings.Load();
-
- // Guard: refuse to call OidcClient when the authority is
- // empty. IdentityModel would otherwise build a bogus
- // authorize URL like "http://127.0.0.1:1/" from an empty
- // Authority, which the browser refuses with a confusing
- // "Cette adresse est interdite"-style message. Tell the
- // operator exactly what to fix instead.
- if (string.IsNullOrWhiteSpace(Settings.Authentication?.Authority))
- {
- IsBusy = false;
- StatusMessage =
- $"Configuration manquante — édite {SettingsFileHint()} et renseigne Authentication.Authority";
- return;
- }
-
- // 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;
-
- // Surface the discovery URL the client is about to call,
- // so a failure (DNS, TLS, 404) can be diagnosed by
- // pasting the URL straight into a browser. OidcClient
- // computes the discovery URL as
- // `Authority + /.well-known/openid-configuration`; we
- // normalise the trailing slash here so the printed URL is
- // exactly what IdentityModel will fetch.
- if (!string.IsNullOrEmpty(DiscoveryUrl))
- StatusMessage = $"Discovering {DiscoveryUrl}";
-
- // Build (or reuse) the API client. The browser override
- // takes precedence: tests want to inject a fake browser
- // and the production path uses Platform.CreateBrowser.
- _api ??= ApiClientOverride ?? new YavscApiClient(Settings, BuildTokenStore());
-
- // Platform.CreateBrowser may still want to be customised
- // per-call (e.g. between desktop and android), so route
- // the interactive login through a callback that reuses
- // BrowserFactoryOverride when present.
- //
- // The progress sink drives Phase / PhaseLabel; StatusMessage
- // keeps the text detail (URLs, error messages). Same
- // underlying flow, two views.
- var progress = new Progress(p => Phase = p);
- await LoginInteractiveCoreAsync(_api, progress);
-
- IsBusy = false;
- AccessToken = _api.CurrentAccessToken;
- StatusMessage = "Interactive token acquired.";
- LoginSuccess = true;
- LoginSucceeded?.Invoke();
- }
- catch (Exception ex)
- {
- IsBusy = false;
- var suffix = !string.IsNullOrEmpty(DiscoveryUrl) ? $" (discovery: {DiscoveryUrl})" : string.Empty;
- StatusMessage = $"Error: {ex.Message}{suffix}";
- }
- }
-
- ///
- /// Single entry point for the OIDC login: YavscApiClient owns the
- /// browser choice, the OidcClient instance, the token persistence
- /// and the refresh path. The VM is just a thin coordinator.
- ///
- private async Task LoginInteractiveCoreAsync(
- YavscApiClient api,
- IProgress? progress = null)
- {
- var original = Platform.CreateBrowser;
- try
- {
- if (BrowserFactoryOverride is not null)
- Platform.CreateBrowser = BrowserFactoryOverride;
-
- await api.LoginInteractiveAsync(progress).ConfigureAwait(false);
- }
- finally
- {
- Platform.CreateBrowser = original;
- }
- }
-
- ///
- /// XDG-compliant path to the user settings file. Surfaced in the
- /// "Configuration manquante" message so the operator knows exactly
- /// which file to edit without having to dig through docs.
- ///
- private static string SettingsFileHint()
- {
- var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- return Path.Combine(appData, "PostIt", "postit-settings.json");
- }
-
- ///
- /// Build the on-disk used by
- /// . The token bundle lives in
- /// ~/.config/PostIt/tokens.json on Linux; the same path
- /// layout is used on every platform for predictability.
- ///
- private static TokenStore BuildTokenStore()
- {
- var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- var path = Path.Combine(appData, "PostIt", "tokens.json");
- return new TokenStore(path);
- }
-}
diff --git a/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs b/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
index 0ebe73a9..9902008f 100644
--- a/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Services;
@@ -9,7 +11,11 @@ namespace PostIt.ViewModels;
/// MainWindow.axaml. Mirrors 's
/// session state ("Connecté" / "Déconnecté") and exposes a
/// Logout command that purges the token store and asks the
-/// navigation owner to route the user back to HomePage.
+/// navigation owner to route the user back to HomePage, plus
+/// a Login command that drives the OIDC interactive flow
+/// and raises a event on success so
+/// MainWindow can push MainPage on top of
+/// HomePage.
///
/// Construction is deferred until the API client exists; the
/// App.axaml.cs wiring sets after building both,
@@ -21,12 +27,29 @@ public partial class SessionStatusViewModel : ViewModelBase
/// App.axaml.cs listens and swaps the navigation root.
public event System.Action? LogoutCompleted;
+ /// Raised after acquired a valid session;
+ /// App.axaml.cs listens and pushes MainPage on top of
+ /// HomePage so the user lands on the blog editor.
+ public event System.Action? LoginSucceeded;
+
[ObservableProperty]
public partial bool IsLoggedIn { get; private set; }
+ /// Inverse of , for XAML bindings
+ /// (the banner shows the Login button when the user is logged out).
+ /// Updated from .
+ [ObservableProperty]
+ public partial bool IsLoggedOut { get; private set; } = true;
+
[ObservableProperty]
public partial string SessionLabel { get; private set; } = "Déconnecté";
+ /// True while a Login flow is in flight; the Login button
+ /// binds IsEnabled to !IsBusy via
+ /// 's CanExecute.
+ [ObservableProperty]
+ public partial bool IsBusy { get; private set; }
+
/// The API client backing the banner. Set once at startup;
/// the banner polls HasValidSession on demand rather than
/// subscribing to a stream — the session state only changes at
@@ -50,9 +73,58 @@ public partial class SessionStatusViewModel : ViewModelBase
{
var has = Api?.HasValidSession ?? false;
IsLoggedIn = has;
+ IsLoggedOut = !has;
SessionLabel = has ? "Connecté" : "Déconnecté";
}
+ ///
+ /// Override the banner label with an error message. Used when
+ /// an interactive login attempt fails so the operator sees
+ /// something on the persistent UI without us needing a
+ /// dedicated error page. The next call
+ /// reverts to "Connecté" / "Déconnecté".
+ ///
+ public void SetError(string message)
+ {
+ IsLoggedIn = false;
+ IsLoggedOut = true;
+ SessionLabel = message;
+ }
+
+ ///
+ /// Drive the OIDC interactive login. On success, refreshes
+ /// the banner state and raises so
+ /// the navigation owner can push MainPage. On failure,
+ /// surfaces the error in the banner via .
+ ///
+ [RelayCommand(CanExecute = nameof(CanLogin))]
+ public async Task LoginAsync()
+ {
+ if (Api is null) return;
+ IsBusy = true;
+ try
+ {
+ await Api.LoginInteractiveAsync().ConfigureAwait(true);
+ }
+ catch (Exception ex)
+ {
+ SetError($"Login failed: {ex.Message}");
+ return;
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+
+ Refresh();
+ if (Api.HasValidSession)
+ LoginSucceeded?.Invoke();
+ }
+
+ private bool CanLogin() => !IsBusy;
+
+ partial void OnIsBusyChanged(bool value) => LoginCommand.NotifyCanExecuteChanged();
+
[RelayCommand]
public async System.Threading.Tasks.Task LogoutAsync()
{
diff --git a/src/PostIt/PostIt/Views/HomePage.axaml b/src/PostIt/PostIt/Views/HomePage.axaml
index bb7204d6..82473542 100644
--- a/src/PostIt/PostIt/Views/HomePage.axaml
+++ b/src/PostIt/PostIt/Views/HomePage.axaml
@@ -9,8 +9,5 @@
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center"/>
-
diff --git a/src/PostIt/PostIt/Views/HomePage.axaml.cs b/src/PostIt/PostIt/Views/HomePage.axaml.cs
index a22b8549..8f564302 100644
--- a/src/PostIt/PostIt/Views/HomePage.axaml.cs
+++ b/src/PostIt/PostIt/Views/HomePage.axaml.cs
@@ -1,9 +1,4 @@
-
using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Microsoft.Extensions.DependencyInjection;
-using PostIt.Services;
-using PostIt.ViewModels;
namespace PostIt.Views;
@@ -13,26 +8,4 @@ public partial class HomePage : ContentPage
{
InitializeComponent();
}
-
- private void OnLoginClick(object? sender, RoutedEventArgs e)
- {
- var vm = (HomePageViewModel)DataContext!;
- // Resolve the next view-model from the same DI container that
- // produced vm.Settings. Constructing them with `new` would
- // instantiate a second Settings and reintroduce the
- // postit://callback crash we just fixed in Settings.cs.
- var services = (App.Current as App)?.Services
- ?? throw new System.InvalidOperationException(
- "App.Services is not bound. OnFrameworkInitializationCompleted must run before any view handler.");
- var loginVm = services.GetRequiredService();
- loginVm.LoginSucceeded += () =>
- {
- var client = services.GetRequiredService();
- Navigation?.PushAsync(new MainPage
- {
- DataContext = services.GetRequiredService()
- });
- };
- Navigation?.PushAsync(new LoginPage { DataContext = loginVm });
- }
}
diff --git a/src/PostIt/PostIt/Views/LoginPage.axaml b/src/PostIt/PostIt/Views/LoginPage.axaml
deleted file mode 100644
index 7969c98a..00000000
--- a/src/PostIt/PostIt/Views/LoginPage.axaml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PostIt/PostIt/Views/LoginPage.axaml.cs b/src/PostIt/PostIt/Views/LoginPage.axaml.cs
deleted file mode 100644
index 8a4e4b1e..00000000
--- a/src/PostIt/PostIt/Views/LoginPage.axaml.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Diagnostics;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Microsoft.Extensions.DependencyInjection;
-using PostIt.ViewModels;
-
-namespace PostIt.Views;
-
-public partial class LoginPage : ContentPage
-{
- public LoginPage()
- {
- InitializeComponent();
-
- // HomePage pushes LoginPage via PushModalAsync(new LoginPage())
- // without supplying a DataContext. Attach a freshly-built
- // LoginPageViewModel whenever the caller hasn't wired one up,
- // so XAML bindings and LoginAsyncCommand resolve. We resolve
- // through the DI container (not `new LoginPageViewModel()`)
- // so the LoginPageViewModel shares the canonical Settings
- // singleton with the rest of the app — constructing a fresh
- // VM here was the original source of the two-Settings
- // postit://callback crash.
- if (DataContext is null)
- {
- var services = (App.Current as App)?.Services
- ?? throw new InvalidOperationException(
- "App.Services is not bound. OnFrameworkInitializationCompleted must run before any view handler.");
- DataContext = services.GetRequiredService();
- }
- }
-
- private async void OnCancelClick(object? sender, RoutedEventArgs e)
- {
- // Cancel button dismisses all open modals
- if (Navigation is not null)
- await Navigation.PopAllModalsAsync();
- }
-
- private void OnRegisterClick(object? sender, RoutedEventArgs e)
- {
- OpenExternalUrl((DataContext as LoginPageViewModel)?.RegisterUrl);
- }
-
- private void OnForgotPasswordClick(object? sender, RoutedEventArgs e)
- {
- OpenExternalUrl((DataContext as LoginPageViewModel)?.ForgotPasswordUrl);
- }
-
- private static void OpenExternalUrl(string? url)
- {
- if (string.IsNullOrEmpty(url)) return;
- // Desktop launcher: shell-execute the URL so the OS picks the right handler.
- // Platform projects (PostIt.Android, PostIt.Browser) override this behavior
- // when they plug into the LoginPage lifecycle.
- try
- {
- Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Failed to open external URL {url}: {ex.Message}");
- }
- }
-}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml
index 247bf28e..4ae9a713 100644
--- a/src/PostIt/PostIt/Views/MainPage.axaml
+++ b/src/PostIt/PostIt/Views/MainPage.axaml
@@ -80,8 +80,7 @@
-
-
-
+
diff --git a/src/PostIt/PostIt/Views/MainWindow.axaml b/src/PostIt/PostIt/Views/MainWindow.axaml
index 79a931c4..32dd038a 100644
--- a/src/PostIt/PostIt/Views/MainWindow.axaml
+++ b/src/PostIt/PostIt/Views/MainWindow.axaml
@@ -10,13 +10,13 @@
Root layout: persistent session banner on top, navigation
surface below. The banner is the single source of truth for
"Connecté / Déconnecté" and the logout button — visible on
- every page (HomePage, LoginPage, MainPage) so the user never
- has to dig through a menu to find their session state.
+ every page (HomePage, MainPage) so the user never has to dig
+ through a menu to find their session state.
The navigation stack is built programmatically in
App.OnFrameworkInitializationCompleted: HomePage is the
root, MainPage is pushed on top when the silent refresh
- succeeds at boot.
+ succeeds at boot or after a fresh login from HomePage.
-->
+