diff --git a/src/PostIt.Tests/LoginPageViewModelTests.cs b/src/PostIt.Tests/LoginPageViewModelTests.cs
index 42390121..38f4c76e 100644
--- a/src/PostIt.Tests/LoginPageViewModelTests.cs
+++ b/src/PostIt.Tests/LoginPageViewModelTests.cs
@@ -41,4 +41,64 @@ public class LoginPageViewModelTests
vm.StatusMessage?.StartsWith("Error") == true,
$"Login reported error: {vm.StatusMessage}");
}
+
+ [Fact]
+ public void RegisterUrl_and_ForgotPasswordUrl_are_derived_from_authority()
+ {
+ var settings = new PostIt.Settings
+ {
+ Authentication = new AuthenticationSettings
+ {
+ Authority = "https://yavsc.example.com/",
+ ClientId = "postit-tests"
+ },
+ RedirectUri = "http://127.0.0.1:7890/",
+ Scopes = new[] { "openid" }
+ };
+
+ var vm = new LoginPageViewModel(settings);
+
+ // Trailing slash on Authority is normalised away.
+ Assert.Equal(
+ "https://yavsc.example.com/signin?ReturnUrl=~%2F&AllowRememberLogin=true",
+ vm.RegisterUrl);
+ Assert.Equal(
+ "https://yavsc.example.com/Account/ForgotPassword",
+ vm.ForgotPasswordUrl);
+ Assert.True(vm.HasRegisterUrl);
+ Assert.True(vm.HasForgotPasswordUrl);
+ }
+
+ [Fact]
+ public void RegisterUrl_is_empty_when_authority_is_unset()
+ {
+ var vm = new LoginPageViewModel(new PostIt.Settings());
+ Assert.Equal(string.Empty, vm.RegisterUrl);
+ Assert.Equal(string.Empty, vm.ForgotPasswordUrl);
+ Assert.False(vm.HasRegisterUrl);
+ Assert.False(vm.HasForgotPasswordUrl);
+ }
+
+ [Fact]
+ public void ConfigMissing_is_true_when_authority_is_unset()
+ {
+ var vm = new LoginPageViewModel(new PostIt.Settings());
+ Assert.True(vm.ConfigMissing);
+ Assert.Contains("~/.config/PostIt/postit-settings.json", vm.ConfigMissingMessage);
+ }
+
+ [Fact]
+ public void ConfigMissing_is_false_when_authority_is_set()
+ {
+ var settings = new PostIt.Settings
+ {
+ Authentication = new AuthenticationSettings
+ {
+ Authority = "https://yavsc.example.com/",
+ ClientId = "postit-tests"
+ }
+ };
+ var vm = new LoginPageViewModel(settings);
+ Assert.False(vm.ConfigMissing);
+ }
}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
index f7fb25bc..6a22cbe7 100644
--- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
@@ -12,6 +12,49 @@ public partial class LoginPageViewModel : ViewModelBase
public string UserEmail { get; set; }
public string Password { get; set; }
+ ///
+ /// URL of the Yavsc.Org register/sign-in page for new users.
+ /// Derived from 's Authority.
+ /// Empty when the authority is not configured.
+ ///
+ public string RegisterUrl =>
+ BuildExternalUrl("/signin?ReturnUrl=~%2F&AllowRememberLogin=true");
+
+ ///
+ /// 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);
+
+ ///
+ /// 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;
+ }
+
private string _AccessToken;
public string AccessToken { get => _AccessToken; private set => this.SetProperty(ref _AccessToken, value); }
@@ -36,6 +79,11 @@ public partial class LoginPageViewModel : ViewModelBase
public LoginPageViewModel() : this(new Settings(), browserFactoryOverride: null)
{
+ // Load settings eagerly so RegisterUrl / ForgotPasswordUrl are
+ // populated as soon as the page renders (XAML bindings fire
+ // before the user clicks Login).
+ try { Settings.Load().GetAwaiter().GetResult(); }
+ catch { /* settings may be missing in tests/dev; LoginAsync will surface real errors */ }
}
///
diff --git a/src/PostIt/PostIt/Views/LoginPage.axaml b/src/PostIt/PostIt/Views/LoginPage.axaml
index f7b9fc53..87c8844e 100644
--- a/src/PostIt/PostIt/Views/LoginPage.axaml
+++ b/src/PostIt/PostIt/Views/LoginPage.axaml
@@ -4,36 +4,58 @@
x:Class="PostIt.Views.LoginPage"
x:DataType="vm:LoginPageViewModel"
Header="Login">
+
+
+
-
+
-
+
+
+
-
-
-
-
+
-
-
-
-
+
+
+
+
-
+
+
+
+
-
+
-
-
+
-
+
+
+
+
+
+
+
+
diff --git a/src/PostIt/PostIt/Views/LoginPage.axaml.cs b/src/PostIt/PostIt/Views/LoginPage.axaml.cs
index 85f51312..01fa1c0c 100644
--- a/src/PostIt/PostIt/Views/LoginPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/LoginPage.axaml.cs
@@ -1,44 +1,23 @@
using System;
-using System.Threading.Tasks;
+using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Interactivity;
-using IdentityModel.OidcClient;
-using PostIt.Services;
+using PostIt.ViewModels;
namespace PostIt.Views;
public partial class LoginPage : ContentPage
{
- private TaskCompletionSource _tcs = new();
- private LoginResult loginResult;
-
-
- public Settings Settings { get; }
-
public LoginPage()
{
- this.Settings = new Settings();
InitializeComponent();
- }
- private async void OnLoginClickAsync(object? sender, RoutedEventArgs e)
- {
- // This is where you specify your actual login auth logic
- try
- {
- Settings.Load().Wait();
-
- var client = new OidcClient(Settings.GetOidcClientOptions());
- var loginResult = await client.LoginAsync(new LoginRequest());
- this.loginResult = loginResult;
- }
- catch (Exception)
- {
- this.loginResult = null;
- }
-
- if (Navigation is not null)
- await Navigation.PopModalAsync();
+ // 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.
+ if (DataContext is null)
+ DataContext = new LoginPageViewModel();
}
private async void OnCancelClick(object? sender, RoutedEventArgs e)
@@ -47,4 +26,30 @@ public partial class LoginPage : ContentPage
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