/* * 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.Labs.WinUI.MarkdownTextBlock; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ProtonVPN.Api.Contracts.Common; using ProtonVPN.Client; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Services.Activation; using ProtonVPN.Client.Core.Services.Navigation; using ProtonVPN.Client.Logic.Auth.Contracts; using ProtonVPN.Client.Logic.Auth.Contracts.Enums; using ProtonVPN.Client.Logic.Servers.Cache; using ProtonVPN.Client.Logic.Servers.Contracts; using ProtonVPN.Client.Logic.Users.Contracts; using ProtonVPN.Client.Settings.Contracts; namespace ProtonVPN.Client.UI.Main; public partial class NoServersPageViewModel : PageViewModelBase { private const int MIN_LOAD_TIME_IN_MS = 2000; private const string REFRESH_ACTION_CODE = "Refresh"; private const string SIGNOUT_ACTION_CODE = "SignOut"; private readonly IServersUpdater _serversUpdater; private readonly IServersCache _serversCache; private readonly IUserAuthenticator _userAuthenticator; private readonly IReportIssueWindowActivator _reportIssueWindowActivator; private readonly IVpnPlanUpdater _vpnPlanUpdater; private readonly IMainWindowActivator _mainWindowActivator; private readonly ISettings _settings; private BaseResponseDetail? AuthResponseDetails => _vpnPlanUpdater.AuthResponseDetails; public override string Title => Localizer.Get(string.IsNullOrEmpty(AuthResponseDetails?.Title) ? "NoServers_Title" : AuthResponseDetails.Title); public string Description => Localizer.Get( HasServersRequestFailed ? "NoServers_FailedToLoad" : !string.IsNullOrEmpty(AuthResponseDetails?.Body) ? AuthResponseDetails.Body : _serversCache.IsEmpty() ? "NoServers_Tip" : "NoServers_UnderMaintenance_Tip"); public bool HasServersRequestFailed => _serversCache.HasServersRequestFailed(); public bool IsRefreshButtonVisible => GetIsActionVisible(REFRESH_ACTION_CODE); public bool IsSignOutButtonVisible => GetIsActionVisible(SIGNOUT_ACTION_CODE); public bool IsHelpSectionVisible => AuthResponseDetails is null && HasServersRequestFailed; public bool IsMarkdownHintVisible => !string.IsNullOrWhiteSpace(HintMarkdown); public string HintMarkdown => AuthResponseDetails?.HintWithMarkdown ?? string.Empty; public MarkdownConfig MarkdownConfig { get; } = MarkdownConfig.Default; public bool IsDebugModeEnabled => _settings.IsDebugModeEnabled; [ObservableProperty] private bool _isRefreshing; public NoServersPageViewModel( IServersUpdater serversUpdater, IServersCache serversCache, IUserAuthenticator userAuthenticator, IReportIssueWindowActivator reportIssueWindowActivator, IVpnPlanUpdater vpnPlanUpdater, IMainWindowActivator mainWindowActivator, IMainWindowViewNavigator parentViewNavigator, IMainViewNavigator childViewNavigator, ISettings settings, IViewModelHelper viewModelHelper) : base(parentViewNavigator, childViewNavigator, viewModelHelper) { _serversUpdater = serversUpdater; _serversCache = serversCache; _userAuthenticator = userAuthenticator; _reportIssueWindowActivator = reportIssueWindowActivator; _vpnPlanUpdater = vpnPlanUpdater; _mainWindowActivator = mainWindowActivator; _settings = settings; } protected override void OnActivated() { base.OnActivated(); DisableMainWindowResizeCapabilities(); } protected override void OnDeactivated() { IsRefreshing = false; RestoreMainWindowResizeCapabilities(); base.OnDeactivated(); } private bool GetIsActionVisible(string actionCode) { if (AuthResponseDetails?.Actions is null) { return true; } return AuthResponseDetails.Actions.Any(a => string.Equals(a.Code, actionCode, StringComparison.OrdinalIgnoreCase)); } [RelayCommand(CanExecute = nameof(CanRefresh))] private async Task RefreshAsync() { try { IsRefreshing = true; // Add some fake waiting time in case the response is fast, // so we prevent the user from refreshing too often await Task.WhenAll( AuthResponseDetails is null ? _serversUpdater.ForceUpdateAsync() : AutoLoginWithoutWindowRepositionAsync(), Task.Delay(MIN_LOAD_TIME_IN_MS)); } finally { bool isNavigatingAwayFromNoServers = _userAuthenticator.IsLoggedIn && !_serversCache.HasNoServers(); if (!isNavigatingAwayFromNoServers) { IsRefreshing = false; } } } private bool CanRefresh() { return !IsRefreshing; } [RelayCommand] private Task SignOutAsync() { return _userAuthenticator.LogoutAsync(LogoutReason.UserAction); } [RelayCommand] private void ContactUs() { _reportIssueWindowActivator.Activate(); } [RelayCommand(CanExecute = nameof(CanSkipNoConnectionsPage))] private Task SkipNoConnectionsPageAsync() { _settings.SkipNoConnectionsPage = true; return ParentViewNavigator.NavigateToDefaultAsync(); } private bool CanSkipNoConnectionsPage() { return IsDebugModeEnabled; } private async Task AutoLoginWithoutWindowRepositionAsync() { _mainWindowActivator.SetWindowMovable(isMovable: false); try { await _userAuthenticator.AutoLoginUserAsync(isAppStartup: false); } finally { _mainWindowActivator.SetWindowMovable(isMovable: true); } } private void DisableMainWindowResizeCapabilities() { if (_mainWindowActivator.Window is MainWindow mainWindow) { mainWindow.InvalidateWindowResizeCapabilities(canResize: false); } } private void RestoreMainWindowResizeCapabilities() { if (_mainWindowActivator.Window is MainWindow mainWindow) { mainWindow.InvalidateTitleBarVisibility(isTitleBarVisible: _userAuthenticator.IsLoggedIn); mainWindow.InvalidateWindowResizeCapabilities(canResize: _userAuthenticator.IsLoggedIn); } } }