diff --git a/browser/ui/webui/brave_vpn/brave_vpn_localized_string_provider.cc b/browser/ui/webui/brave_vpn/brave_vpn_localized_string_provider.cc index b98de934c932..59aa18aca145 100644 --- a/browser/ui/webui/brave_vpn/brave_vpn_localized_string_provider.cc +++ b/browser/ui/webui/brave_vpn/brave_vpn_localized_string_provider.cc @@ -111,6 +111,8 @@ void AddLocalizedStrings(content::WebUIDataSource* html_source) { {"braveVpnSettingsTooltip", IDS_BRAVE_VPN_MAIN_PANEL_VPN_SETTINGS_TITLE}, {"braveVpnSessionExpiredContent", IDS_BRAVE_VPN_MAIN_PANEL_SESSION_EXPIRED_PART_CONTENT}, + {"braveVpnOutOfCredentials", + IDS_BRAVE_VPN_MAIN_PANEL_OUT_OF_CREDENTIALS_TITLE}, }; for (const auto& str : kLocalizedStrings) { diff --git a/components/brave_vpn/browser/brave_vpn_service.cc b/components/brave_vpn/browser/brave_vpn_service.cc index e37884581a08..0061207afa87 100644 --- a/components/brave_vpn/browser/brave_vpn_service.cc +++ b/components/brave_vpn/browser/brave_vpn_service.cc @@ -368,6 +368,33 @@ void BraveVpnService::UpdatePurchasedStateForSessionExpired( return; } + // Person ran out of credentials. + // This should only happen if communication bewteen client and VPN provider + // is lost after the credential is redeemed (multiple times). + if (out_of_credentials_) { + const auto last_credential_expiry = + local_prefs_->GetTime(prefs::kBraveVPNLastCredentialExpiry); + if (!last_credential_expiry.is_null()) { + std::stringstream ss; + base::TimeDelta delta = (last_credential_expiry - base::Time::Now()); + ss << "Out of credentials; check again in "; + if (delta.InHours() == 0) { + ss << delta.InMinutes() << " minutes."; + } else { + int delta_hours = delta.InHours(); + ss << delta.InHours() << " hours "; + base::TimeDelta delta2 = (delta - base::Hours(delta_hours)); + ss << delta2.InMinutes() << " minutes."; + } + VLOG(2) << __func__ << ss.str(); + SetPurchasedState( + env, PurchasedState::OUT_OF_CREDENTIALS, + l10n_util::GetStringUTF8( + IDS_BRAVE_VPN_MAIN_PANEL_OUT_OF_CREDENTIALS_CONTENT)); + return; + } + } + // Weird state. Maybe we don't see this condition. // Just checking for safe. if (session_expired_time > base::Time::Now()) { @@ -619,6 +646,7 @@ void BraveVpnService::OnCredentialSummary(const std::string& domain, VLOG(1) << __func__ << " : Treat it as not purchased state in android."; SetPurchasedState(env, PurchasedState::NOT_PURCHASED); #else + out_of_credentials_ = true; VLOG(1) << __func__ << " : Treat it as session expired state in desktop."; UpdatePurchasedStateForSessionExpired(env); #endif @@ -678,6 +706,7 @@ void BraveVpnService::OnPrepareCredentialsPresentation( return; } + out_of_credentials_ = false; SetSkusCredential(local_prefs_, credential, time); if (GetCurrentEnvironment() != env) { @@ -716,28 +745,28 @@ void BraveVpnService::OnGetSubscriberCredentialV12( return; } - // If we get here, we've already tried two credentials (the retry failed). + // We can set the state as FAILED and do not attempt to get another + // credential. The cached credential will eventually expire and user will + // fetch a new one. + // + // There could be two reasons for this. + + // 1. We've already tried two credentials (the retry failed). if (token_no_longer_valid && IsRetriedSkusCredential(local_prefs_)) { VLOG(2) << __func__ << " : Got TokenNoLongerValid again with retried skus credential"; + out_of_credentials_ = true; + SetPurchasedState( + GetCurrentEnvironment(), PurchasedState::FAILED, + l10n_util::GetStringUTF8(IDS_BRAVE_VPN_PURCHASE_TOKEN_NOT_VALID)); + return; } - // When this path is reached: - // - The cached credential is considered good but vendor side has an error. - // That could be a network outage or a server side error on vendor side. - // OR - // - The cached credential is consumed and we've now tried two different - // credentials. - // - // We set the state as FAILED and do not attempt to get another credential. - // Cached credential will eventually expire and user will fetch a new one. - // - // This logic can be updated if we issue more than two credentials per day. - auto message_id = token_no_longer_valid - ? IDS_BRAVE_VPN_PURCHASE_TOKEN_NOT_VALID - : IDS_BRAVE_VPN_PURCHASE_CREDENTIALS_FETCH_FAILED; + // 2. The cached credential is considered good but vendor side has an error. + // That could be a network outage or a server side error on vendor side. SetPurchasedState(GetCurrentEnvironment(), PurchasedState::FAILED, - l10n_util::GetStringUTF8(message_id)); + l10n_util::GetStringUTF8( + IDS_BRAVE_VPN_PURCHASE_CREDENTIALS_FETCH_FAILED)); #endif return; } diff --git a/components/brave_vpn/browser/brave_vpn_service.h b/components/brave_vpn/browser/brave_vpn_service.h index b1884b5f2cf5..5721f8918602 100644 --- a/components/brave_vpn/browser/brave_vpn_service.h +++ b/components/brave_vpn/browser/brave_vpn_service.h @@ -253,6 +253,7 @@ class BraveVpnService : std::unique_ptr delegate_; base::RepeatingTimer p3a_timer_; base::OneShotTimer subs_cred_refresh_timer_; + bool out_of_credentials_ = false; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/brave_vpn/browser/brave_vpn_service_helper.cc b/components/brave_vpn/browser/brave_vpn_service_helper.cc index da07d0990fd5..cf987e5e6b13 100644 --- a/components/brave_vpn/browser/brave_vpn_service_helper.cc +++ b/components/brave_vpn/browser/brave_vpn_service_helper.cc @@ -86,6 +86,7 @@ void SetSkusCredential(PrefService* local_prefs, base::TimeToValue(expiration_time)); local_prefs->SetDict(prefs::kBraveVPNSubscriberCredential, std::move(cred_dict)); + local_prefs->SetTime(prefs::kBraveVPNLastCredentialExpiry, expiration_time); } void SetSkusCredentialFetchingRetried(PrefService* local_prefs, bool retried) { diff --git a/components/brave_vpn/common/brave_vpn_utils.cc b/components/brave_vpn/common/brave_vpn_utils.cc index 63acf9f68dc9..61d6d75a5f76 100644 --- a/components/brave_vpn/common/brave_vpn_utils.cc +++ b/components/brave_vpn/common/brave_vpn_utils.cc @@ -43,6 +43,7 @@ void RegisterVPNLocalStatePrefs(PrefRegistrySimple* registry) { registry->RegisterStringPref(prefs::kBraveVPNWireguardProfileCredentials, ""); registry->RegisterDictionaryPref(prefs::kBraveVPNRootPref); registry->RegisterDictionaryPref(prefs::kBraveVPNSubscriberCredential); + registry->RegisterTimePref(prefs::kBraveVPNLastCredentialExpiry, {}); registry->RegisterBooleanPref(prefs::kBraveVPNLocalStateMigrated, false); registry->RegisterTimePref(prefs::kBraveVPNSessionExpiredDate, {}); #if BUILDFLAG(ENABLE_BRAVE_VPN_WIREGUARD) diff --git a/components/brave_vpn/common/mojom/brave_vpn.mojom b/components/brave_vpn/common/mojom/brave_vpn.mojom index ce6c5d67eda9..31891ad875ad 100644 --- a/components/brave_vpn/common/mojom/brave_vpn.mojom +++ b/components/brave_vpn/common/mojom/brave_vpn.mojom @@ -114,6 +114,7 @@ enum PurchasedState { LOADING, SESSION_EXPIRED, FAILED, + OUT_OF_CREDENTIALS, }; struct PurchasedInfo { diff --git a/components/brave_vpn/common/pref_names.h b/components/brave_vpn/common/pref_names.h index 5b8bfe47e90c..2947761fd8a8 100644 --- a/components/brave_vpn/common/pref_names.h +++ b/components/brave_vpn/common/pref_names.h @@ -60,6 +60,10 @@ inline constexpr char kBraveVPNEnvironment[] = "brave.brave_vpn.env"; // Dict that has subscriber credential its expiration date. inline constexpr char kBraveVPNSubscriberCredential[] = "brave.brave_vpn.subscriber_credential"; +// Set the expiry of the last redeemed credential. +// Useful in case redemption fails and person uses all credentials. +inline constexpr char kBraveVPNLastCredentialExpiry[] = + "brave.brave_vpn.last_credential_expiry"; // Time that session expired occurs. inline constexpr char kBraveVPNSessionExpiredDate[] = diff --git a/components/brave_vpn/resources/panel/components/main-panel/index.tsx b/components/brave_vpn/resources/panel/components/main-panel/index.tsx index c33f04b87eb7..6a24363cc9d9 100644 --- a/components/brave_vpn/resources/panel/components/main-panel/index.tsx +++ b/components/brave_vpn/resources/panel/components/main-panel/index.tsx @@ -106,7 +106,9 @@ function MainPanel() { const isSelectingRegion = useSelector((state) => state.isSelectingRegion) const connectionStatus = useSelector((state) => state.connectionStatus) const expired = useSelector((state) => state.expired) + const outOfCredentials = useSelector((state) => state.outOfCredentials) const regions = useSelector((state) => state.regions) + const stateDescription = useSelector((state) => state.stateDescription) const onSelectRegionButtonClick = () => { dispatch(Actions.toggleRegionSelector(true)) @@ -184,6 +186,16 @@ function MainPanel() { )} + {outOfCredentials && ( + +
{getLocale('braveVpnOutOfCredentials')}
+
{stateDescription}
+
+ )} { state === ConnectionState.CONNECT_FAILED ? ConnectionState.DISCONNECTED : state /* Treat connection failure on startup as disconnected */, - expired: false + expired: false, + outOfCredentials: false, + stateDescription: '' }) ) }) @@ -86,7 +88,28 @@ handler.on(Actions.purchaseExpired.getType(), async (store) => { currentRegion, regions, connectionStatus: ConnectionState.DISCONNECTED, - expired: true + expired: true, + outOfCredentials: false, + stateDescription: '' + }) + ) +}) + +handler.on(Actions.outOfCredentials.getType(), async (store) => { + const [{ state }, { currentRegion }, { regions }] = await Promise.all([ + getPanelBrowserAPI().serviceHandler.getPurchasedState(), + getPanelBrowserAPI().serviceHandler.getSelectedRegion(), + getPanelBrowserAPI().serviceHandler.getAllRegions() + ]) + + store.dispatch( + Actions.showMainView({ + currentRegion, + regions, + connectionStatus: ConnectionState.DISCONNECTED, + expired: false, + stateDescription: state.description || '', + outOfCredentials: true }) ) }) diff --git a/components/brave_vpn/resources/panel/state/reducer.ts b/components/brave_vpn/resources/panel/state/reducer.ts index 17f6650bf0fe..355528a531e8 100644 --- a/components/brave_vpn/resources/panel/state/reducer.ts +++ b/components/brave_vpn/resources/panel/state/reducer.ts @@ -13,6 +13,7 @@ type RootState = { hasError: boolean isSelectingRegion: boolean expired: boolean + outOfCredentials: boolean connectionStatus: ConnectionState regions: Region[] currentRegion: Region @@ -25,6 +26,7 @@ const defaultState: RootState = { hasError: false, isSelectingRegion: false, expired: false, + outOfCredentials: false, connectionStatus: ConnectionState.DISCONNECTED, regions: [], currentRegion: new Region(), @@ -148,9 +150,11 @@ reducer.on(Actions.showMainView, (state, payload): RootState => { return { ...state, expired: payload.expired, + outOfCredentials: payload.outOfCredentials, currentRegion: payload.currentRegion, regions: payload.regions, connectionStatus: payload.connectionStatus, + stateDescription: payload.stateDescription, currentView: ViewType.Main } }) diff --git a/components/brave_vpn/resources/panel/state/store.ts b/components/brave_vpn/resources/panel/state/store.ts index 983f932820ee..8246744ee44c 100644 --- a/components/brave_vpn/resources/panel/state/store.ts +++ b/components/brave_vpn/resources/panel/state/store.ts @@ -32,6 +32,9 @@ const observer = { case PurchasedState.SESSION_EXPIRED: store.dispatch(Actions.purchaseExpired()) break + case PurchasedState.OUT_OF_CREDENTIALS: + store.dispatch(Actions.outOfCredentials()) + break case PurchasedState.FAILED: store.dispatch(Actions.purchaseFailed({ state: PurchasedState.FAILED, stateDescription: description diff --git a/components/resources/brave_vpn_strings.grdp b/components/resources/brave_vpn_strings.grdp index 6e974a1b00f7..aadf24a2ccaf 100644 --- a/components/resources/brave_vpn_strings.grdp +++ b/components/resources/brave_vpn_strings.grdp @@ -182,6 +182,10 @@ Credentials expiry date elapsed. + + Brave VPN is having trouble authenticating to the VPN server. Please contact support or try again later. + + Unable to load credentials. @@ -367,6 +371,9 @@ Let's get you connected! Session expired + + Can't authenticate to VPN + Settings