/* * 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 CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; 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.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.UI.Login.Bases; namespace ProtonVPN.Client.UI.Login.Pages; public partial class TwoFactorPageViewModel : LoginPageViewModelBase { private readonly IEventMessageSender _eventMessageSender; private readonly IUserAuthenticator _userAuthenticator; private readonly IGuestHoleManager _guestHoleManager; private readonly IUrlsBrowser _urlsBrowser; [ObservableProperty] private bool _isToShowError; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AuthenticateWithCodeCommand))] [NotifyCanExecuteChangedFor(nameof(AuthenticateWithSecurityKeyCommand))] private bool _isAuthenticating; [ObservableProperty] private bool _isAuthenticatorModeSelected; [ObservableProperty] private bool _isSecurityKeyModeSelected; public bool CanSwitchTwoFactorMode => _userAuthenticator.IsTwoFactorAuthenticatorModeEnabled && _userAuthenticator.IsTwoFactorSecurityKeyModeEnabled; public string LearnMoreUrl => _urlsBrowser.TwoFactorAuthLearnMore; public TwoFactorPageViewModel( ILoginViewNavigator parentViewNavigator, IEventMessageSender eventMessageSender, IUserAuthenticator userAuthenticator, IGuestHoleManager guestHoleManager, IUrlsBrowser urlsBrowser, IViewModelHelper viewModelHelper) : base(parentViewNavigator, viewModelHelper) { _eventMessageSender = eventMessageSender; _userAuthenticator = userAuthenticator; _guestHoleManager = guestHoleManager; _urlsBrowser = urlsBrowser; } public event EventHandler? OnTwoFactorFailure; public event EventHandler? OnTwoFactorSuccess; [RelayCommand(CanExecute = nameof(CanAuthenticate))] public async Task AuthenticateWithCodeAsync(string twoFactorCode) { if (twoFactorCode is not { Length: 6 }) { IsToShowError = true; return; } IsToShowError = false; try { IsAuthenticating = true; _eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Authenticating)); AuthResult result = await _userAuthenticator.SendTwoFactorCodeAsync(twoFactorCode); await HandleAuthResultAsync(result); } catch (Exception ex) { _eventMessageSender.Send(new LoginStateChangedMessage(LoginState.TwoFactorFailed, AuthError.Unknown, ex.Message)); } finally { IsAuthenticating = false; } } [RelayCommand(CanExecute = nameof(CanAuthenticate))] public async Task AuthenticateWithSecurityKeyAsync() { try { IsAuthenticating = true; _eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Authenticating)); AuthResult result = await _userAuthenticator.AuthenticateWithSecurityKeyAsync(); await HandleAuthResultAsync(result); } catch (Exception ex) { _eventMessageSender.Send(new LoginStateChangedMessage(LoginState.TwoFactorFailed, AuthError.Unknown, ex.Message)); } finally { IsAuthenticating = false; } } public void Receive(LoginStateChangedMessage message) { ExecuteOnUIThread(() => { if (message.Value == LoginState.TwoFactorFailed) { OnTwoFactorFailure?.Invoke(this, EventArgs.Empty); } }); } protected override void OnActivated() { base.OnActivated(); InvalidateTwoFactorMode(); } protected override void OnDeactivated() { IsToShowError = false; } private async Task HandleAuthResultAsync(AuthResult result) { if (result.Success) { if (_guestHoleManager.IsActive) { await _guestHoleManager.DisconnectAsync(); } _eventMessageSender.Send(new LoginStateChangedMessage(LoginState.Success)); OnTwoFactorSuccess?.Invoke(this, EventArgs.Empty); } else { _eventMessageSender.Send(result.Value == AuthError.TwoFactorCancelled ? new LoginStateChangedMessage(LoginState.TwoFactorCancelled) : new LoginStateChangedMessage(LoginState.TwoFactorFailed, result.Value)); } } private bool CanAuthenticate() { return !IsAuthenticating; } [RelayCommand] private async Task GoBackAsync() { if (_guestHoleManager.IsActive) { await _guestHoleManager.DisconnectAsync(); } return await ParentViewNavigator.NavigateToSignInViewAsync(); } private void InvalidateTwoFactorMode() { OnPropertyChanged(nameof(CanSwitchTwoFactorMode)); if (CanSwitchTwoFactorMode && (IsAuthenticatorModeSelected || IsSecurityKeyModeSelected)) { // If both modes are available and one of them is already selected, do nothing. return; } if (_userAuthenticator.IsTwoFactorSecurityKeyModeEnabled) { IsSecurityKeyModeSelected = true; IsAuthenticatorModeSelected = false; } else if (_userAuthenticator.IsTwoFactorAuthenticatorModeEnabled) { IsAuthenticatorModeSelected = true; IsSecurityKeyModeSelected = false; } else { _eventMessageSender.Send(new LoginStateChangedMessage(LoginState.TwoFactorFailed, AuthError.Unknown, Localizer.Get("Login_TwoFactor_NoMethodAvailable"))); GoBackCommand.Execute(null); } } }