/* * Copyright (c) 2026 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.UI.Xaml.Navigation; using ProtonVPN.Client.Common.Enums; using ProtonVPN.Client.Common.Models; using ProtonVPN.Client.Contracts.Services.Browsing; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Enums; using ProtonVPN.Client.Core.Messages; using ProtonVPN.Client.Core.Services.Activation; using ProtonVPN.Client.Core.Services.Navigation; using ProtonVPN.Client.Core.Services.Selection; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Logic.Auth.Contracts.Enums; using ProtonVPN.Client.Logic.Auth.Contracts.Messages; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Client.Settings.Contracts.Messages; using ProtonVPN.Client.UI.Login.Pages; using ProtonVPN.Logging.Contracts.Events.AppLogs; using ProtonVPN.Logging.Contracts.Events.GuestHoleLogs; namespace ProtonVPN.Client.UI.Login; public partial class LoginPageViewModel : PageViewModelBase, IEventMessageReceiver, IEventMessageReceiver, IEventMessageReceiver { private readonly IUrlsBrowser _urlsBrowser; private readonly IMainWindowActivator _mainWindowActivator; private readonly IMainWindowOverlayActivator _mainWindowOverlayActivator; private readonly IReportIssueWindowActivator _reportIssueWindowActivator; private readonly ITroubleshootingWindowActivator _troubleshootingWindowActivator; private readonly ISettings _settings; private readonly IDebugToolsWindowActivator _debugToolsWindowActivator; private readonly IApplicationIconSelector _applicationIconSelector; [ObservableProperty] private string _message; [ObservableProperty] private bool _isMessageVisible; [ObservableProperty] private Severity _messageSeverity = Severity.Error; [ObservableProperty] private bool _isHelpVisible; [ObservableProperty] private string _actionButtonTitle = string.Empty; public bool IsDebugModeEnabled => _settings.IsDebugModeEnabled; public LoginPageViewModel( IUrlsBrowser urlsBrowser, IMainWindowActivator mainWindowActivator, IMainWindowOverlayActivator mainWindowOverlayActivator, IReportIssueWindowActivator reportIssueWindowActivator, ITroubleshootingWindowActivator troubleshootingWindowActivator, ISettings settings, IDebugToolsWindowActivator debugToolsWindowActivator, IApplicationIconSelector applicationIconSelector, IMainWindowViewNavigator parentViewNavigator, ILoginViewNavigator childViewNavigator, IViewModelHelper viewModelHelper) : base(parentViewNavigator, childViewNavigator, viewModelHelper) { _urlsBrowser = urlsBrowser; _mainWindowActivator = mainWindowActivator; _mainWindowOverlayActivator = mainWindowOverlayActivator; _reportIssueWindowActivator = reportIssueWindowActivator; _troubleshootingWindowActivator = troubleshootingWindowActivator; _settings = settings; _debugToolsWindowActivator = debugToolsWindowActivator; _applicationIconSelector = applicationIconSelector; _message = string.Empty; } public void Receive(LoginStateChangedMessage message) { ActionButtonTitle = string.Empty; ExecuteOnUIThread(async () => { switch (message.Value) { case LoginState.Authenticating: ClearMessage(); break; case LoginState.Success: ClearMessage(); break; case LoginState.TwoFactorRequired: ClearMessage(); await ChildViewNavigator.NavigateToTwoFactorViewAsync(); break; case LoginState.TwoFactorFailed: switch (message.AuthError) { case AuthError.IncorrectTwoFactorCode: SetErrorMessage(Localizer.Get("Login_Error_IncorrectTwoFactorCode")); await ChildViewNavigator.NavigateToTwoFactorViewAsync(); break; case AuthError.TwoFactorAuthFailed: SetErrorMessage(Localizer.Get("Login_Error_TwoFactorFailed")); await ChildViewNavigator.NavigateToSignInViewAsync(); break; case AuthError.WebAuthnNotSupported: SetErrorMessage(Localizer.Get("Login_Error_WebAuthnNotSupported")); await ChildViewNavigator.NavigateToSignInViewAsync(); break; case AuthError.NoVpnAccess: await ParentViewNavigator.NavigateToNoServersViewAsync(); break; case AuthError.Unknown: SetErrorMessage(message.ErrorMessage); break; } break; case LoginState.TwoFactorCancelled: await ChildViewNavigator.NavigateToSignInViewAsync(); break; case LoginState.Error: await ChildViewNavigator.NavigateToSignInViewAsync(); HandleAuthErrorAsync(message); break; } }); } public void Receive(LoggedOutMessage message) { ExecuteOnUIThread(() => { switch (message.Reason) { case LogoutReason.UserAction: break; case LogoutReason.SessionExpired: SetErrorMessage(Localizer.Get("Login_Error_SessionExpired")); break; } }); } public void Receive(SettingChangedMessage message) { ExecuteOnUIThread(() => { if (message.PropertyName == nameof(ISettings.IsKillSwitchEnabled) && !_settings.IsKillSwitchEnabled && ParentViewNavigator.GetCurrentPageContext() is LoginPageViewModel && ChildViewNavigator.GetCurrentPageContext() is SignInPageViewModel) { SetMessage(Localizer.Get("SignIn_KillSwitch_Disabled"), Severity.Success); } }); } private async void HandleAuthErrorAsync(LoginStateChangedMessage message) { ActionButtonTitle = string.Empty; if (message.AuthError == AuthError.None) { return; } switch (message.AuthError) { case AuthError.MissingSrpDll: Logger.Fatal("The app is missing proton_srp_cffi.dll"); await _mainWindowOverlayActivator.ShowMessageAsync(new MessageDialogParameters { Title = Localizer.Get("Login_Error_MissingSrpDll_Title"), Message = Localizer.Get("Login_Error_MissingSrpDll_Message"), PrimaryButtonText = Localizer.Get("Common_Actions_GotIt"), UseVerticalLayoutForButtons = true, }); _mainWindowActivator.Exit(); break; case AuthError.GuestHoleFailed: Logger.Error("Failed to authenticate using guest hole."); _troubleshootingWindowActivator.Activate(); break; case AuthError.GuestHoleFailedDueToMobileHotspot: Logger.Error("Failed to authenticate using guest hole due to mobile hotspot."); SetErrorMessage(Localizer.Get("Login_Error_GuestHoleFailedDueToMobileHotspot")); ActionButtonTitle = Localizer.Get("Connection_Error_ReportAnIssue"); break; case AuthError.SsoAuthFailed: SetErrorMessage(Localizer.Get("Login_Error_SsoAuthFailed")); break; case AuthError.GetSessionDetailsFailed: SetErrorMessage(Localizer.Get("Login_Error_GetSessionDetailsFailed")); break; default: SetErrorMessage(message.ErrorMessage); break; } } protected override void OnChildNavigation(NavigationEventArgs e) { base.OnChildNavigation(e); IsHelpVisible = e.Content is not LoadingPageView; } [RelayCommand] public void ResetPassword() { _urlsBrowser.BrowseTo(_urlsBrowser.ResetPassword); } [RelayCommand] public void ForgotUsername() { _urlsBrowser.BrowseTo(_urlsBrowser.ForgotUsername); } [RelayCommand] public void TroubleSigningIn() { _urlsBrowser.BrowseTo(_urlsBrowser.TroubleSigningIn); } [RelayCommand] public Task ReportAnIssueAsync() { return _reportIssueWindowActivator.ActivateAsync(); } [RelayCommand(CanExecute = nameof(CanShowDebugTools))] private void ShowDebugTools() { _debugToolsWindowActivator.Activate(); } private bool CanShowDebugTools() { return IsDebugModeEnabled; } private void SetErrorMessage(string message) { SetMessage(message, Severity.Error); } private void SetMessage(string message, Severity severity) { if (string.IsNullOrWhiteSpace(message)) { Logger.Warn($"Tried to set an empty error message on login page. Stack trace: {Environment.StackTrace}"); ClearMessage(); return; } MessageSeverity = severity; Message = message; IsMessageVisible = true; } private void ClearMessage() { IsMessageVisible = false; } protected override void OnActivated() { base.OnActivated(); if (_mainWindowActivator.Window is MainWindow mainWindow) { mainWindow.InvalidateWindowResizeCapabilities(canResize: false); } } protected override void OnDeactivated() { base.OnDeactivated(); } partial void OnIsMessageVisibleChanged(bool value) { if (IsMessageVisible && MessageSeverity is Severity.Error or Severity.Warning) { _applicationIconSelector.OnAuthenticationErrorTriggered(MessageSeverity); } else { _applicationIconSelector.OnAuthenticationErrorDismissed(); } } [RelayCommand] private Task TriggerActionButtonAsync() { return _reportIssueWindowActivator.ActivateAsync(); } }