/*
* Copyright (c) 2025 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 System.Security;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ProtonVPN.Api.Contracts;
using ProtonVPN.Client.Contracts.Services.Browsing;
using ProtonVPN.Client.Core.Bases;
using ProtonVPN.Client.Core.Enums;
using ProtonVPN.Client.Core.Messages;
using ProtonVPN.Client.Core.Services.Navigation;
using ProtonVPN.Client.EventMessaging.Contracts;
using ProtonVPN.Client.Logic.Auth;
using ProtonVPN.Client.Logic.Auth.Contracts;
using ProtonVPN.Client.Logic.Auth.Contracts.Enums;
using ProtonVPN.Client.Logic.Auth.Contracts.Models;
using ProtonVPN.Client.Logic.Connection.Contracts.GuestHole;
using ProtonVPN.Client.Settings.Contracts;
using ProtonVPN.Client.UI.Login.Bases;
using ProtonVPN.Client.UI.Login.Enums;
using ProtonVPN.Client.UI.Login.Overlays;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Common.Legacy.Abstract;
using ProtonVPN.Logging.Contracts.Events.UserLogs;
using Windows.System;
namespace ProtonVPN.Client.UI.Login.Pages;
public partial class SignInPageViewModel : LoginPageViewModelBase
{
private readonly IUrlsBrowser _urlsBrowser;
private readonly IUserAuthenticator _userAuthenticator;
private readonly IEventMessageSender _eventMessageSender;
private readonly IApiAvailabilityVerifier _apiAvailabilityVerifier;
private readonly IGuestHoleManager _guestHoleManager;
private readonly ISessionSettings _sessionSettings;
private readonly IUnauthSessionManager _unauthSessionManager;
private readonly SsoLoginOverlayViewModel _ssoLoginOverlayViewModel;
private readonly IMainWindowViewNavigator _mainWindowViewNavigator;
public event EventHandler? OnPasswordClearRequested;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SignInCommand))]
[NotifyPropertyChangedFor(nameof(IsSignInFormEnabled))]
private bool _isSigningIn;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SignInCommand))]
[NotifyPropertyChangedFor(nameof(UsernameFieldLabel))]
[NotifyPropertyChangedFor(nameof(SwitchPageLabel))]
[NotifyPropertyChangedFor(nameof(IsSrpFormType))]
[NotifyPropertyChangedFor(nameof(IsSsoFormType))]
[NotifyPropertyChangedFor(nameof(IsSwitchFormButtonVisible))]
private SignInFormType _signInFormType;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SignInCommand))]
private string _username = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SignInCommand))]
private SecureString _password = new();
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(CreateAccountCommand))]
private bool _isToShowCreateAccountSpinner;
[ObservableProperty]
private bool _isToShowUsernameError;
[ObservableProperty]
private bool _isToShowPasswordError;
private AuthError _authError;
public bool IsSignInFormEnabled => !IsSigningIn;
public bool IsSrpFormType => SignInFormType == SignInFormType.SRP;
public bool IsSsoFormType => SignInFormType == SignInFormType.SSO;
public bool IsSwitchFormButtonVisible => SignInFormType == SignInFormType.SRP;
public string UsernameFieldLabel => SignInFormType switch
{
SignInFormType.SRP => Localizer.Get("SignIn_Form_Username"),
SignInFormType.SSO => Localizer.Get("SignIn_Form_Email"),
_ => Localizer.Get("SignIn_Form_Username")
};
public string SwitchPageLabel => SignInFormType switch
{
SignInFormType.SRP => Localizer.Get("SignIn_Form_SignInWithSso"),
_ => string.Empty
};
public string CreateAccountUrl => _urlsBrowser.CreateAccount;
public AuthError AuthError => _authError;
private bool CanCreateAccount => !IsToShowCreateAccountSpinner;
public SignInPageViewModel(
ILoginViewNavigator parentViewNavigator,
IUrlsBrowser urlsBrowser,
IUserAuthenticator userAuthenticator,
IEventMessageSender eventMessageSender,
IApiAvailabilityVerifier apiAvailabilityVerifier,
IGuestHoleManager guestHoleManager,
ISessionSettings sessionSettings,
IUnauthSessionManager unauthSessionManager,
SsoLoginOverlayViewModel ssoLoginOverlayViewModel,
IViewModelHelper viewModelHelper,
IMainWindowViewNavigator mainWindowViewNavigator)
: base(parentViewNavigator, viewModelHelper)
{
_urlsBrowser = urlsBrowser;
_userAuthenticator = userAuthenticator;
_eventMessageSender = eventMessageSender;
_apiAvailabilityVerifier = apiAvailabilityVerifier;
_guestHoleManager = guestHoleManager;
_sessionSettings = sessionSettings;
_unauthSessionManager = unauthSessionManager;
_ssoLoginOverlayViewModel = ssoLoginOverlayViewModel;
_mainWindowViewNavigator = mainWindowViewNavigator;
}
[RelayCommand(CanExecute = nameof(CanSignIn))]
public async Task SignInAsync()
{
if (!ValidateForm())
{
return;
}
try
{
IsSigningIn = true;
_eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Authenticating));
AuthResult result = SignInFormType switch
{
SignInFormType.SRP => await HandleSrpLoginAsync(),
SignInFormType.SSO => await HandleSsoLoginAsync(),
_ => throw new NotSupportedException($"{SignInFormType} login is not supported.")
};
if (result.Success)
{
HandleSuccess();
}
else
{
HandleError(result);
}
}
catch (OperationCanceledException)
{
Logger.Info("Authentication was cancelled by the user.");
}
catch (Exception e)
{
Logger.Error("An error occurred during authentication.", e);
_eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Error, AuthError.Unknown, e.Message));
}
finally
{
IsSigningIn = false;
}
}
private bool ValidateForm()
{
switch (SignInFormType)
{
case SignInFormType.SRP:
IsToShowUsernameError = string.IsNullOrWhiteSpace(Username);
IsToShowPasswordError = Password is null || Password.Length == 0;
break;
case SignInFormType.SSO:
Username = Username?.Trim() ?? string.Empty;
IsToShowUsernameError = string.IsNullOrWhiteSpace(Username) || !Username.IsValidEmailAddress();
break;
}
return !IsToShowUsernameError && !IsToShowPasswordError;
}
private bool CanSignIn()
{
return !IsSigningIn;
}
private async Task HandleSrpLoginAsync()
{
// Copy password so it can be cleared immediately without affecting the ongoing authentication.
SecureString password = Password;
try
{
return await _userAuthenticator.LoginUserAsync(Username, password);
}
finally
{
Password = new();
OnPasswordClearRequested?.Invoke(this, EventArgs.Empty);
}
}
private async Task HandleSsoLoginAsync()
{
SsoAuthResult result = await _userAuthenticator.StartSsoAuthAsync(Username);
return result.Success
? await _ssoLoginOverlayViewModel.AuthenticateAsync(result.SsoChallengeToken)
: result;
}
private void HandleSuccess()
{
_authError = AuthError.None;
_eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Success));
}
private void HandleError(AuthResult result)
{
_authError = result.Value;
switch (result.Value)
{
case AuthError.TwoFactorRequired:
_eventMessageSender.Send(new LoginStateChangedMessage(LoginState.TwoFactorRequired));
break;
case AuthError.NoVpnAccess:
_mainWindowViewNavigator.NavigateToNoServersViewAsync();
break;
case AuthError.SwitchToSSO:
case AuthError.SwitchToSRP:
SwitchLoginForm();
goto default;
default:
_eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Error, result.Value, result.Error));
break;
}
}
[RelayCommand]
public void SwitchLoginForm()
{
SignInFormType = SignInFormType switch
{
SignInFormType.SRP => SignInFormType.SSO,
SignInFormType.SSO => SignInFormType.SRP,
_ => SignInFormType.SRP,
};
}
[RelayCommand(CanExecute = nameof(CanCreateAccount))]
public async Task CreateAccountAsync()
{
try
{
IsToShowCreateAccountSpinner = true;
bool isSignUpPageAccessible = await _apiAvailabilityVerifier.IsSignUpPageAccessibleAsync();
if (isSignUpPageAccessible)
{
await OpenCreateAccountPageAsync();
}
else
{
await _guestHoleManager.ExecuteAsync(OpenCreateAccountPageAsync);
}
}
finally
{
IsToShowCreateAccountSpinner = false;
}
}
private async Task OpenCreateAccountPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(_urlsBrowser.CreateAccount));
return Result.Ok();
}
protected override void OnActivated()
{
base.OnActivated();
_unauthSessionManager.Revoke();
if (_userAuthenticator.IsAutoLogin != true && SignInCommand.CanExecute(null))
{
if (!string.IsNullOrEmpty(_sessionSettings.Username))
{
Username = _sessionSettings.Username.Trim();
if (!string.IsNullOrEmpty(_sessionSettings.Password))
{
Password = _sessionSettings.Password.Trim().ToSecureString();
SignInCommand.Execute(null);
}
}
}
}
partial void OnSignInFormTypeChanged(SignInFormType oldValue, SignInFormType newValue)
{
IsToShowUsernameError = false;
IsToShowPasswordError = false;
}
}