/*
* 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.Activation;
using ProtonVPN.Client.EventMessaging.Contracts;
using ProtonVPN.Client.Localization.Extensions;
using ProtonVPN.Client.Logic.Connection.Contracts;
using ProtonVPN.Client.Logic.Connection.Contracts.Enums;
using ProtonVPN.Client.Logic.Connection.Contracts.Extensions;
using ProtonVPN.Client.Logic.Connection.Contracts.Messages;
using ProtonVPN.Client.Logic.Connection.Contracts.Models;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Features;
using ProtonVPN.Client.Logic.Profiles.Contracts.Messages;
using ProtonVPN.Client.Logic.Profiles.Contracts.Models;
using ProtonVPN.Client.Logic.Recents.Contracts;
using ProtonVPN.Client.Logic.Recents.Contracts.Messages;
using ProtonVPN.Client.Logic.Servers.Contracts;
using ProtonVPN.Client.Logic.Servers.Contracts.Enums;
using ProtonVPN.Client.Logic.Servers.Contracts.Extensions;
using ProtonVPN.Client.Logic.Servers.Contracts.Messages;
using ProtonVPN.Client.Logic.Users.Contracts.Messages;
using ProtonVPN.Client.Settings.Contracts;
using ProtonVPN.Client.Settings.Contracts.Messages;
using ProtonVPN.StatisticalEvents.Contracts.Dimensions;
namespace ProtonVPN.Client.UI.Main.Home.Card;
public partial class ConnectionCardComponentViewModel : ActivatableViewModelBase,
IEventMessageReceiver,
IEventMessageReceiver,
IEventMessageReceiver,
IEventMessageReceiver,
IEventMessageReceiver,
IEventMessageReceiver,
IEventMessageReceiver
{
private const int FREE_COUNTRIES_DISPLAYED_AS_FLAGS = 3;
private readonly IConnectionManager _connectionManager;
private readonly ISettings _settings;
private readonly IRecentConnectionsManager _recentConnectionsManager;
private readonly IMainWindowOverlayActivator _mainWindowOverlayActivator;
private readonly IServersLoader _serversLoader;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ConnectCommand))]
[NotifyCanExecuteChangedFor(nameof(CancelConnectionCommand))]
[NotifyCanExecuteChangedFor(nameof(DisconnectCommand))]
[NotifyCanExecuteChangedFor(nameof(ShowFreeConnectionsOverlayCommand))]
[NotifyPropertyChangedFor(nameof(Profile))]
[NotifyPropertyChangedFor(nameof(IsProfileIntent))]
[NotifyPropertyChangedFor(nameof(Title))]
[NotifyPropertyChangedFor(nameof(Subtitle))]
[NotifyPropertyChangedFor(nameof(HasSubtitle))]
[NotifyPropertyChangedFor(nameof(HasSubtitleOrFeature))]
[NotifyPropertyChangedFor(nameof(IsDisconnected))]
[NotifyPropertyChangedFor(nameof(IsConnecting))]
[NotifyPropertyChangedFor(nameof(IsConnected))]
[NotifyPropertyChangedFor(nameof(IsFreeConnectionsTaglineVisible))]
[NotifyPropertyChangedFor(nameof(IsChangeServerOptionVisible))]
[NotifyPropertyChangedFor(nameof(IsChangeDefaultConnectionOptionVisible))]
[NotifyPropertyChangedFor(nameof(ExitCountry))]
[NotifyPropertyChangedFor(nameof(EntryCountry))]
[NotifyPropertyChangedFor(nameof(IsSecureCore))]
[NotifyPropertyChangedFor(nameof(IsTor))]
[NotifyPropertyChangedFor(nameof(IsP2P))]
[NotifyPropertyChangedFor(nameof(IsB2B))]
[NotifyPropertyChangedFor(nameof(FlagType))]
private ConnectionStatus _currentConnectionStatus;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Profile))]
[NotifyPropertyChangedFor(nameof(IsProfileIntent))]
[NotifyPropertyChangedFor(nameof(Title))]
[NotifyPropertyChangedFor(nameof(Subtitle))]
[NotifyPropertyChangedFor(nameof(HasSubtitle))]
[NotifyPropertyChangedFor(nameof(HasSubtitleOrFeature))]
[NotifyPropertyChangedFor(nameof(ExitCountry))]
[NotifyPropertyChangedFor(nameof(EntryCountry))]
[NotifyPropertyChangedFor(nameof(IsSecureCore))]
[NotifyPropertyChangedFor(nameof(IsTor))]
[NotifyPropertyChangedFor(nameof(IsP2P))]
[NotifyPropertyChangedFor(nameof(IsB2B))]
[NotifyPropertyChangedFor(nameof(FlagType))]
private IConnectionIntent? _currentConnectionIntent;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Profile))]
[NotifyPropertyChangedFor(nameof(IsProfileIntent))]
[NotifyPropertyChangedFor(nameof(Title))]
[NotifyPropertyChangedFor(nameof(Subtitle))]
[NotifyPropertyChangedFor(nameof(HasSubtitle))]
[NotifyPropertyChangedFor(nameof(HasSubtitleOrFeature))]
[NotifyPropertyChangedFor(nameof(ExitCountry))]
[NotifyPropertyChangedFor(nameof(EntryCountry))]
[NotifyPropertyChangedFor(nameof(IsSecureCore))]
[NotifyPropertyChangedFor(nameof(IsTor))]
[NotifyPropertyChangedFor(nameof(IsP2P))]
[NotifyPropertyChangedFor(nameof(IsB2B))]
[NotifyPropertyChangedFor(nameof(FlagType))]
private ConnectionDetails? _currentConnectionDetails;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedFreeCountriesCount))]
private int _freeCountriesCount;
public string FormattedFreeCountriesCount => FreeCountriesCount > FREE_COUNTRIES_DISPLAYED_AS_FLAGS
? $"+{FreeCountriesCount - FREE_COUNTRIES_DISPLAYED_AS_FLAGS}"
: string.Empty;
public IConnectionProfile? Profile => CurrentConnectionStatus switch
{
ConnectionStatus.Connected => CurrentConnectionDetails?.OriginalConnectionIntent as IConnectionProfile,
_ => CurrentConnectionIntent as IConnectionProfile
};
public bool IsProfileIntent => Profile != null;
public string Title => GetConnectionCardTitle();
public string Subtitle => GetConnectionCardSubtitle();
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
public bool HasSubtitleOrFeature => HasSubtitle || IsTor || IsP2P;
public bool IsDisconnected => CurrentConnectionStatus == ConnectionStatus.Disconnected;
public bool IsConnecting => CurrentConnectionStatus == ConnectionStatus.Connecting;
public bool IsConnected => CurrentConnectionStatus == ConnectionStatus.Connected;
public bool IsFreeUser => !_settings.VpnPlan.IsPaid;
public bool IsFreeConnectionsTaglineVisible => IsFreeUser && !IsConnected;
public bool IsChangeServerOptionVisible => IsFreeUser && IsConnected;
public bool IsChangeDefaultConnectionOptionVisible => !IsFreeUser && IsDisconnected;
public string? ExitCountry =>
CurrentConnectionStatus switch
{
ConnectionStatus.Connected => CurrentConnectionDetails?.ExitCountryCode,
_ => CurrentConnectionIntent?.Location?.GetCountryCode()
};
public string? EntryCountry =>
CurrentConnectionStatus switch
{
ConnectionStatus.Connected =>
CurrentConnectionDetails?.OriginalConnectionIntent.Feature is SecureCoreFeatureIntent secureCoreIntent && !secureCoreIntent.IsFastest
? CurrentConnectionDetails.EntryCountryCode
: string.Empty,
_ => (CurrentConnectionIntent?.Feature as SecureCoreFeatureIntent)?.EntryCountryCode
};
public bool IsSecureCore => IsFeature(ServerFeatures.SecureCore);
public bool IsTor => IsFeature(ServerFeatures.Tor);
public bool IsP2P => IsFeature(ServerFeatures.P2P);
public bool IsB2B => IsFeature(ServerFeatures.B2B);
public FlagType FlagType => (CurrentConnectionStatus switch
{
ConnectionStatus.Connected => CurrentConnectionDetails?.OriginalConnectionIntent.Location,
_ => CurrentConnectionIntent?.Location
}).GetFlagType(CurrentConnectionStatus is ConnectionStatus.Connected);
public ConnectionCardComponentViewModel(
IViewModelHelper viewModelHelper,
IConnectionManager connectionManager,
ISettings settings,
IRecentConnectionsManager recentConnectionsManager,
IMainWindowOverlayActivator mainWindowOverlayActivator,
IServersLoader serversLoader)
: base(viewModelHelper)
{
_connectionManager = connectionManager;
_settings = settings;
_recentConnectionsManager = recentConnectionsManager;
_mainWindowOverlayActivator = mainWindowOverlayActivator;
_serversLoader = serversLoader;
}
public void Receive(ConnectionStatusChangedMessage message)
{
ExecuteOnUIThread(() =>
{
if (IsActive)
{
InvalidateConnectionStatus();
if (message.ConnectionStatus == ConnectionStatus.Connected)
{
InvalidateConnectionDetails();
}
else
{
InvalidateConnectionIntent();
}
}
});
}
public void Receive(ServerListChangedMessage message)
{
if (IsActive)
{
ExecuteOnUIThread(() =>
{
InvalidateFreeCountriesCount();
InvalidateConnectionIntent();
});
}
}
public void Receive(LocationNamesChangedMessage message)
{
if (IsActive)
{
ExecuteOnUIThread(() => OnPropertyChanged(nameof(Subtitle)));
}
}
public void Receive(VpnPlanChangedMessage message)
{
if (IsActive)
{
ExecuteOnUIThread(InvalidateVpnPlan);
}
}
public void Receive(RecentConnectionsChangedMessage message)
{
if (IsActive)
{
ExecuteOnUIThread(InvalidateConnectionIntent);
}
}
public void Receive(ProfilesChangedMessage message)
{
if (IsActive)
{
ExecuteOnUIThread(InvalidateConnectionIntent);
}
}
public void Receive(SettingChangedMessage message)
{
if (IsActive && message.PropertyName == nameof(ISettings.DefaultConnection))
{
ExecuteOnUIThread(InvalidateConnectionIntent);
}
}
protected override void OnActivated()
{
base.OnActivated();
InvalidateConnectionStatus();
InvalidateConnectionIntent();
InvalidateConnectionDetails();
InvalidateFreeCountriesCount();
InvalidateVpnPlan();
}
protected override void OnLanguageChanged()
{
base.OnLanguageChanged();
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Subtitle));
}
[RelayCommand(CanExecute = nameof(CanConnect))]
private Task ConnectAsync()
{
return _connectionManager.ConnectAsync(VpnTriggerDimension.ConnectionCard, CurrentConnectionIntent);
}
private bool CanConnect()
{
return IsDisconnected;
}
[RelayCommand(CanExecute = nameof(CanCancelConnection))]
private async Task CancelConnectionAsync()
{
await _connectionManager.DisconnectAsync(VpnTriggerDimension.ConnectionCard);
}
private bool CanCancelConnection()
{
return IsConnecting;
}
[RelayCommand(CanExecute = nameof(CanDisconnect))]
private Task DisconnectAsync()
{
return _connectionManager.DisconnectAsync(VpnTriggerDimension.ConnectionCard);
}
private bool CanDisconnect()
{
return IsConnected;
}
[RelayCommand(CanExecute = nameof(CanShowFreeConnectionsOverlay))]
private Task ShowFreeConnectionsOverlayAsync()
{
return _mainWindowOverlayActivator.ShowFreeConnectionsOverlayAsync();
}
private bool CanShowFreeConnectionsOverlay()
{
return IsDisconnected;
}
[RelayCommand]
private Task ShowP2PInfoOverlayAsync()
{
return _mainWindowOverlayActivator.ShowP2PInfoOverlayAsync();
}
[RelayCommand]
private Task ShowTorInfoOverlayAsync()
{
return _mainWindowOverlayActivator.ShowTorInfoOverlayAsync();
}
private void InvalidateConnectionStatus()
{
CurrentConnectionStatus = _connectionManager.ConnectionStatus;
}
private void InvalidateConnectionIntent()
{
IConnectionIntent? currentConnectionIntent = _connectionManager.CurrentConnectionIntent;
CurrentConnectionIntent = _connectionManager.IsDisconnected || currentConnectionIntent == null
? _recentConnectionsManager.GetDefaultConnection()
: currentConnectionIntent;
// If intent is a profile, the reference might be the same even though profile details/settings have changed.
// Force invalidating all properties.
InvalidateAllProperties();
}
private void InvalidateConnectionDetails()
{
CurrentConnectionDetails = _connectionManager.CurrentConnectionDetails;
}
private void InvalidateFreeCountriesCount()
{
FreeCountriesCount = _serversLoader.GetFreeCountries().Count();
}
private void InvalidateVpnPlan()
{
OnPropertyChanged(nameof(IsFreeUser));
OnPropertyChanged(nameof(IsFreeConnectionsTaglineVisible));
OnPropertyChanged(nameof(IsChangeServerOptionVisible));
OnPropertyChanged(nameof(IsChangeDefaultConnectionOptionVisible));
}
private string GetConnectionCardTitle()
{
return IsProfileIntent
? Profile!.Name
: CurrentConnectionStatus switch
{
ConnectionStatus.Connected => Localizer.GetConnectionDetailsTitle(CurrentConnectionDetails),
_ => Localizer.GetConnectionIntentTitle(CurrentConnectionIntent),
};
}
private string GetConnectionCardSubtitle()
{
return CurrentConnectionStatus switch
{
ConnectionStatus.Connected => IsProfileIntent
? Localizer.GetConnectionProfileDetailsSubtitle(CurrentConnectionDetails)
: Localizer.GetConnectionDetailsSubtitle(CurrentConnectionDetails),
_ => IsProfileIntent
? Localizer.GetConnectionProfileSubtitle(Profile)
: Localizer.GetConnectionIntentSubtitle(CurrentConnectionIntent, useDetailedSubtitle: true),
};
}
private bool IsFeature(ServerFeatures serverFeature)
where TFeatureIntent : class, IFeatureIntent
{
return CurrentConnectionStatus switch
{
ConnectionStatus.Connected => CurrentConnectionDetails != null
&& CurrentConnectionDetails?.OriginalConnectionIntent.Feature is TFeatureIntent
&& CurrentConnectionDetails.Server.Features.IsSupported(serverFeature),
_ => CurrentConnectionIntent?.Feature is TFeatureIntent
};
}
}