/*
* 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();
}
}