/* * 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.Common.Enums; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Services.Selection; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Logic.Auth.Contracts.Messages; using ProtonVPN.Client.Logic.Connection.ConnectionErrors; using ProtonVPN.Client.Logic.Connection.Contracts; using ProtonVPN.Client.Logic.Connection.Contracts.ConnectionErrors; using ProtonVPN.Client.Logic.Connection.Contracts.Enums; using ProtonVPN.Client.Logic.Connection.Contracts.Messages; using ProtonVPN.Common.Core.Helpers; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; using ProtonVPN.OperatingSystems.Network.Contracts.NetworkInterfaces; namespace ProtonVPN.Client.UI.Main.Components; public partial class ConnectionErrorViewModel : ViewModelBase, IEventMessageReceiver, IEventMessageReceiver, IEventMessageReceiver { private readonly IConfiguration _config; private readonly IConnectionErrorFactory _connectionErrorFactory; private readonly IApplicationIconSelector _applicationIconSelector; private readonly IConflictingNetworkInterfacesProvider _conflictingNetworkInterfacesProvider; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(TriggerActionButtonCommand))] [NotifyCanExecuteChangedFor(nameof(CloseErrorCommand))] private bool _isConnectionErrorVisible; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ActionButtonTitle))] [NotifyPropertyChangedFor(nameof(ConnectionErrorSeverity))] [NotifyPropertyChangedFor(nameof(ConnectionErrorTitle))] [NotifyPropertyChangedFor(nameof(ConnectionErrorMessage))] [NotifyPropertyChangedFor(nameof(IsConnectionErrorVisible))] [NotifyCanExecuteChangedFor(nameof(TriggerActionButtonCommand))] private IConnectionError? _connectionError; private ConnectionStatus _lastStatus = ConnectionStatus.Disconnected; public Severity ConnectionErrorSeverity => ConnectionError?.Severity ?? Severity.None; public string ConnectionErrorTitle => ConnectionError?.Title ?? string.Empty; public string ConnectionErrorMessage => ConnectionError?.Message ?? string.Empty; public string ActionButtonTitle => ConnectionError?.ActionLabel ?? string.Empty; private readonly Lazy> _conflictingAdapterCheckerDebouncer; public ConnectionErrorViewModel( IConfiguration config, IConnectionErrorFactory connectionErrorFactory, IApplicationIconSelector applicationIconSelector, IViewModelHelper viewModelHelper, IConflictingNetworkInterfacesProvider conflictingNetworkInterfacesProvider) : base(viewModelHelper) { _config = config; _connectionErrorFactory = connectionErrorFactory; _applicationIconSelector = applicationIconSelector; _conflictingNetworkInterfacesProvider = conflictingNetworkInterfacesProvider; _conflictingAdapterCheckerDebouncer = new(() => new(_config.ConflictingAdapterCheckerDelay, input => TriggerConflictingAdapterCheckAsync(input))); } private async Task TriggerConflictingAdapterCheckAsync(ConnectionStatus connectionStatus) { if (connectionStatus is not ConnectionStatus.Connecting) { return; } IReadOnlyList conflictingAdapters = _conflictingNetworkInterfacesProvider.Get(); if (conflictingAdapters.Any()) { ExecuteOnUIThread(() => { SetConflictingAdapterError(conflictingAdapters, connectionStatus); }); } } public void Receive(ConnectionErrorMessage message) { ExecuteOnUIThread(() => { IConnectionError connectionError = _connectionErrorFactory.GetConnectionError(message.VpnError); if (connectionError is WireGuardAdapterInUseConnectionError or TapAdapterInUseConnectionError or UnknownConnectionError) { if (IsConnectionErrorVisible && ConnectionError is IConflictingAdapterConnectionError) { Logger.Info($"Not showing {connectionError.GetType().Name} because there is already a ConflictingAdapterConnectionError being shown."); } else { IReadOnlyList conflictingAdapters = _conflictingNetworkInterfacesProvider.Get(); if (conflictingAdapters.Any()) { SetConflictingAdapterError(conflictingAdapters, _lastStatus); } else { SetConnectionError(connectionError); } } } else { SetConnectionError(connectionError); } }); } private void SetConnectionError(IConnectionError connectionError) { ConnectionError = connectionError; IsConnectionErrorVisible = !string.IsNullOrEmpty(ConnectionErrorTitle) && !string.IsNullOrEmpty(ConnectionErrorMessage); OnPropertyChanged(nameof(ConnectionErrorMessage)); OnPropertyChanged(nameof(ConnectionErrorSeverity)); _applicationIconSelector.OnConnectionErrorTriggered(ConnectionErrorSeverity); } public void Receive(ConnectionStatusChangedMessage message) { if (_lastStatus != message.ConnectionStatus) { _conflictingAdapterCheckerDebouncer.Value.Call(message.ConnectionStatus); } _lastStatus = message.ConnectionStatus; ExecuteOnUIThread(() => { if (message.ConnectionStatus == ConnectionStatus.Connecting && IsToCloseErrorOnConnecting()) { CloseError(); } else if (message.ConnectionStatus == ConnectionStatus.Disconnected && IsToCloseErrorOnDisconnect()) { CloseError(); } else if (message.ConnectionStatus == ConnectionStatus.Connected) { CloseError(); } else if (IsConnectionErrorVisible && ConnectionError is IConflictingAdapterConnectionError conflictingAdapterConnectionError) { conflictingAdapterConnectionError.SetConnectionStatus(message.ConnectionStatus); OnPropertyChanged(nameof(ConnectionErrorSeverity)); _applicationIconSelector.OnConnectionErrorTriggered(ConnectionErrorSeverity); } }); } private void SetConflictingAdapterError(IReadOnlyList conflictingAdapters, ConnectionStatus connectionStatus) { if (ConnectionError is TwoFactorRequiredConnectionError) { return; // Don't set the conflicting adapter error if the current error is 2FA required (even if the message is not being shown) } IConflictingAdapterConnectionError conflictingAdapterConnectionError = _connectionErrorFactory.GetConflictingAdapterConnectionError(); conflictingAdapterConnectionError.SetConflictingAdapters(conflictingAdapters); conflictingAdapterConnectionError.SetConnectionStatus(connectionStatus); SetConnectionError(conflictingAdapterConnectionError); } private bool IsToCloseErrorOnConnecting() { return ConnectionError?.IsToCloseErrorOnConnecting ?? true; } private bool IsToCloseErrorOnDisconnect() { return ConnectionError?.IsToCloseErrorOnDisconnect ?? false; } public void Receive(LoggingOutMessage message) { ExecuteOnUIThread(CloseError); } [RelayCommand(CanExecute = nameof(CanTriggerActionButton))] private async Task TriggerActionButtonAsync() { CloseError(); if (ConnectionError is not null) { await ConnectionError.ExecuteActionAsync(); } } private bool CanTriggerActionButton() { return IsConnectionErrorVisible && !string.IsNullOrEmpty(ActionButtonTitle); } [RelayCommand(CanExecute = nameof(CanCloseError))] private void CloseError() { IsConnectionErrorVisible = false; } private bool CanCloseError() { return IsConnectionErrorVisible; } partial void OnIsConnectionErrorVisibleChanged(bool value) { if (IsConnectionErrorVisible) { _applicationIconSelector.OnConnectionErrorTriggered(ConnectionErrorSeverity); } else { _applicationIconSelector.OnConnectionErrorDismissed(); } } protected override void OnLanguageChanged() { base.OnLanguageChanged(); OnPropertyChanged(nameof(ConnectionErrorTitle)); OnPropertyChanged(nameof(ConnectionErrorMessage)); OnPropertyChanged(nameof(ActionButtonTitle)); } }