tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 00da8ea4a2e3ad744ec0658c1c27784b11ec0430
parent 473167f996f53e159a233ab1261858cb05a3b077
Author: Niklas Baumgardner <nbaumgardner@mozilla.com>
Date:   Thu,  8 Jan 2026 03:39:29 +0000

Bug 2000711 - Create VPN bandwidth usage element. r=ip-protection-reviewers,fluent-reviewers,mkennedy,bolsson,fchasen

Differential Revision: https://phabricator.services.mozilla.com/D276718

Diffstat:
Mbrowser/app/profile/firefox.js | 3+++
Mbrowser/components/ipprotection/IPProtectionUsage.sys.mjs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Abrowser/components/ipprotection/content/bandwidth-usage.css | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/ipprotection/content/bandwidth-usage.mjs | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/content/ipprotection-constants.mjs | 5+++++
Mbrowser/components/ipprotection/content/ipprotection-status-card.mjs | 18++++++++++--------
Mbrowser/components/ipprotection/jar.mn | 2++
Mbrowser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js | 38++++++++++++++++++++++++++++++--------
Mbrowser/components/ipprotection/tests/browser/browser_ipprotection_usage.js | 36++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/tests/browser/head.js | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/tests/xpcshell/test_IPProtectionUsage.js | 14++++++++++----
Mbrowser/components/preferences/main.js | 4++++
Mbrowser/components/preferences/preferences.xhtml | 1+
Mbrowser/components/preferences/privacy.js | 5+++++
Mbrowser/locales-preview/ipProtection.ftl | 24++++++++++++++++++++++++
15 files changed, 521 insertions(+), 22 deletions(-)

diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -1680,6 +1680,7 @@ pref("services.sync.prefs.sync.browser.download.useDownloadDir", true); pref("services.sync.prefs.sync.browser.firefox-view.feature-tour", true); pref("services.sync.prefs.sync.browser.formfill.enable", true); pref("services.sync.prefs.sync.browser.ipProtection.enabled", true); +pref("services.sync.prefs.sync.browser.ipProtection.bandwidth", true); pref("services.sync.prefs.sync.browser.link.open_newwindow", true); pref("services.sync.prefs.sync.browser.menu.showViewImageInfo", true); pref("services.sync.prefs.sync.browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", true); @@ -3565,6 +3566,8 @@ pref("browser.ipProtection.features.siteExceptions", false); pref("browser.ipProtection.log", false); pref("browser.ipProtection.guardian.endpoint", "https://vpn.mozilla.org/"); pref("browser.ipProtection.added", false); +// Pref that stores bandwidth usage in MB +pref("browser.ipProtection.bandwidth", 0); // Pref to enable aboug:glean redesign. pref("about.glean.redesign.enabled", false); diff --git a/browser/components/ipprotection/IPProtectionUsage.sys.mjs b/browser/components/ipprotection/IPProtectionUsage.sys.mjs @@ -2,6 +2,18 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", +}); + +const BANDWIDTH_PREF = "browser.ipProtection.bandwidth"; +const BYTES_TO_MB = 1000000; +// 200 ms for testing, 60 seconds for real scenarios +const BANDWIDTH_WRITE_TIMEOUT = Cu.isInAutomation ? 1000 : 60 * 1000; +const BYTES_TO_GB = 1000000000; + /** * Service Class to observe and record IP protection usage. * @@ -24,6 +36,8 @@ export class IPProtectionUsage { if (this.#active) { return; } + this.#bandwidthUsageSinceLastWrite = 0; + this.#writeBandwidthUsageTask = null; Services.obs.addObserver(this, "http-on-stop-request"); this.#active = true; } @@ -34,6 +48,9 @@ export class IPProtectionUsage { this.#active = false; this.#isolationKeys.clear(); Services.obs.removeObserver(this, "http-on-stop-request"); + this.#writeBandwidthUsageTask?.finalize().then(() => { + this.#writeBandwidthUsageTask = null; + }); } addIsolationKey(key) { @@ -50,7 +67,7 @@ export class IPProtectionUsage { try { const chan = subject.QueryInterface(Ci.nsIHttpChannel); if (this.shouldCountChannel(chan)) { - IPProtectionUsage.countChannel(chan); + this.countChannel(chan); } } catch (err) { // If the channel is not an nsIHttpChannel @@ -79,12 +96,31 @@ export class IPProtectionUsage { return false; } + writeUsage() { + // The pref is synced so we only add the bandwidth usage since + // our last write + const prefUsage = Services.prefs.getIntPref(BANDWIDTH_PREF, 0); + const totalUsage = + this.#bandwidthUsageSinceLastWrite / BYTES_TO_MB + prefUsage; + + // Bandwidth is stored in MB so we covert it when getting/setting + // the pref + Services.prefs.setIntPref(BANDWIDTH_PREF, totalUsage); + this.#bandwidthUsageSinceLastWrite = 0; + } + + forceWriteUsage() { + this.#writeBandwidthUsageTask?.disarm(); + this.writeUsage(); + this.#writeBandwidthUsageTask?.arm(); + } + /** * Checks a completed channel and records the transfer sizes to glean. * * @param {nsIHttpChannel} chan - A completed Channel to check. */ - static countChannel(chan) { + countChannel(chan) { try { const cacheInfo = chan.QueryInterface(Ci.nsICacheInfoChannel); if (cacheInfo.isFromCache()) { @@ -96,14 +132,30 @@ export class IPProtectionUsage { if (chan.transferSize > 0) { Glean.ipprotection.usageRx.accumulate(chan.transferSize); + this.#bandwidthUsageSinceLastWrite += chan.transferSize; } if (chan.requestSize > 0) { Glean.ipprotection.usageTx.accumulate(chan.requestSize); + this.#bandwidthUsageSinceLastWrite += chan.requestSize; + } + + if (!this.#writeBandwidthUsageTask) { + this.#writeBandwidthUsageTask = new lazy.DeferredTask(() => { + this.writeUsage(); + }, BANDWIDTH_WRITE_TIMEOUT); } + + if (this.#bandwidthUsageSinceLastWrite > BYTES_TO_GB) { + this.forceWriteUsage(); + } + + this.#writeBandwidthUsageTask.arm(); } #active = false; #isolationKeys = new Set(); + #bandwidthUsageSinceLastWrite = 0; + #writeBandwidthUsageTask; } IPProtectionUsage.prototype.QueryInterface = ChromeUtils.generateQI([ diff --git a/browser/components/ipprotection/content/bandwidth-usage.css b/browser/components/ipprotection/content/bandwidth-usage.css @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +:host[numeric] { + #progress-description { + color: var(--text-color-deemphasized); + } +} + +.container { + display: flex; + flex-direction: column; + gap: var(--space-medium); +} + +#bandwidth-header { + margin: 0; + font-weight: var(--heading-font-weight); + font-size: var(--heading-font-size-medium); +} + +#usage-help-text { + color: var(--text-color-deemphasized); +} + +#progress-bar { + width: 100%; + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-size-tokens */ + height: 10px; + /* stylelint-disable-next-line stylelint-plugin-mozilla/no-base-design-tokens, stylelint-plugin-mozilla/use-design-tokens */ + background-color: light-dark(var(--color-gray-20), var(--color-gray-80)); + + border: 0.5px solid var(--border-color-card); + border-radius: var(--border-radius-circle); + + &::-moz-progress-bar { + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens, stylelint-plugin-mozilla/no-non-semantic-token-usage */ + background-color: var(--icon-color-success); + border: 0.5px solid var(--border-color-card); + border-radius: var(--border-radius-circle); + box-shadow: var(--box-shadow-level-1); + } + + &[percent="75"]::-moz-progress-bar { + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens, stylelint-plugin-mozilla/no-non-semantic-token-usage */ + background-color: var(--icon-color-warning); + } + + &[percent="90"]::-moz-progress-bar { + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens, stylelint-plugin-mozilla/no-non-semantic-token-usage */ + background-color: var(--icon-color-critical); + } +} diff --git a/browser/components/ipprotection/content/bandwidth-usage.mjs b/browser/components/ipprotection/content/bandwidth-usage.mjs @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { + BANDWIDTH_USAGE, + LINKS, +} from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "CURRENT_BANDWIDTH_USAGE", + "browser.ipProtection.bandwidth", + 0 +); + +/** + * Element used for displaying VPN bandwidth usage. + * By default, the element will display a progress bar and numeric text of the + * available bandwidth. Adding the attribute `numeric` will only display the + * numeric text of available bandwidth. + */ +export default class BandwidthUsageCustomElement extends MozLitElement { + static properties = { + numeric: { type: Boolean, reflect: true }, + }; + + get bandwidthUsedGB() { + return lazy.CURRENT_BANDWIDTH_USAGE / BANDWIDTH_USAGE.MB_TO_GB; + } + + get bandwidthPercent() { + const usageGB = lazy.CURRENT_BANDWIDTH_USAGE / BANDWIDTH_USAGE.MB_TO_GB; + const percent = (100 * usageGB) / BANDWIDTH_USAGE.MAX_BANDWIDTH; + if (percent > 90) { + return 90; + } else if (percent > 75) { + return 75; + } + return percent.toFixed(0); + } + + get bandwidthUsedMB() { + return lazy.CURRENT_BANDWIDTH_USAGE; + } + + get bandwidthUsageLeftGB() { + return BANDWIDTH_USAGE.MAX_BANDWIDTH - this.bandwidthUsedGB; + } + + get bandwidthUsageLeftMB() { + return this.bandwidthUsageLeftGB * BANDWIDTH_USAGE.MB_TO_GB; + } + + get bandwidthUsageLeft() { + if (this.bandwidthUsedGB < 1) { + // Bug 2006997 - Handle this scenario where less than 1 GB used. + return Math.floor(this.bandwidthUsageLeftGB); + } else if (this.bandwidthUsageLeftGB < 1) { + // Show usage left in MB if less than 1GB left + return this.bandwidthUsageLeftMB.toFixed(0); + } + + return this.bandwidthUsageLeftGB.toFixed(0); + } + + get bandwidthLeftDataL10nId() { + if (this.bandwidthUsageLeftGB < 1) { + return "ip-protection-bandwidth-left-mb"; + } + return "ip-protection-bandwidth-left-gb"; + } + + get bandwidthLeftThisMonthDataL10nId() { + if (this.bandwidthUsageLeftGB < 1) { + return "ip-protection-bandwidth-left-this-month-mb"; + } + return "ip-protection-bandwidth-left-this-month-gb"; + } + + constructor() { + super(); + this.numeric = false; + } + + progressBarTemplate() { + if (this.numeric) { + return null; + } + + let descriptionText; + if (this.bandwidthUsedGB < BANDWIDTH_USAGE.MAX_BANDWIDTH) { + descriptionText = html`<span + id="progress-description" + data-l10n-id=${this.bandwidthLeftDataL10nId} + data-l10n-args=${JSON.stringify({ + usageLeft: this.bandwidthUsageLeft, + maxUsage: BANDWIDTH_USAGE.MAX_BANDWIDTH, + })} + ></span>`; + } else { + descriptionText = html`<span + id="progress-description" + data-l10n-id="ip-protection-bandwidth-hit-for-the-month" + data-l10n-args=${JSON.stringify({ + maxUsage: BANDWIDTH_USAGE.MAX_BANDWIDTH, + })} + ></span>`; + } + + return html` + <div class="container"> + <h3 + id="bandwidth-header" + data-l10n-id="ip-protection-bandwidth-header" + ></h3> + <div> + <span + id="usage-help-text" + data-l10n-id="ip-protection-bandwidth-help-text" + data-l10n-args=${JSON.stringify({ + maxUsage: BANDWIDTH_USAGE.MAX_BANDWIDTH, + })} + ></span> + <a + is="moz-support-link" + part="support-link" + support-page=${LINKS.SUPPORT_URL} + ></a> + </div> + <progress + id="progress-bar" + max="150" + value=${this.bandwidthUsedGB} + percent=${this.bandwidthPercent} + ></progress> + ${descriptionText} + </div> + `; + } + + numericTemplate() { + if (!this.numeric) { + return null; + } + + if (this.bandwidthUsedGB < BANDWIDTH_USAGE.MAX_BANDWIDTH) { + return html`<span + id="progress-description" + data-l10n-id=${this.bandwidthLeftThisMonthDataL10nId} + data-l10n-args=${JSON.stringify({ + usageLeft: this.bandwidthUsageLeft, + maxUsage: BANDWIDTH_USAGE.MAX_BANDWIDTH, + })} + ></span>`; + } + + return html`<span + id="progress-description" + data-l10n-id="ip-protection-bandwidth-hit-for-the-month" + data-l10n-args=${JSON.stringify({ + maxUsage: BANDWIDTH_USAGE.MAX_BANDWIDTH, + })} + ></span>`; + } + + render() { + let content = null; + if (this.numeric) { + content = this.numericTemplate(); + } else { + content = this.progressBarTemplate(); + } + + return html`<link + rel="stylesheet" + href="chrome://browser/content/ipprotection/bandwidth-usage.css" + /> + ${content}`; + } +} +customElements.define("bandwidth-usage", BandwidthUsageCustomElement); diff --git a/browser/components/ipprotection/content/ipprotection-constants.mjs b/browser/components/ipprotection/content/ipprotection-constants.mjs @@ -41,3 +41,8 @@ export const ONBOARDING_PREF_FLAGS = { EVER_USED_SITE_EXCEPTIONS: 1 << 1, EVER_TURNED_ON_VPN: 1 << 2, }; + +export const BANDWIDTH_USAGE = Object.freeze({ + MAX_BANDWIDTH: 150, + MB_TO_GB: 1000, +}); diff --git a/browser/components/ipprotection/content/ipprotection-status-card.mjs b/browser/components/ipprotection/content/ipprotection-status-card.mjs @@ -13,6 +13,8 @@ import { import "chrome://global/content/elements/moz-toggle.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/ipprotection/ipprotection-site-settings-control.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/ipprotection/bandwidth-usage.mjs"; /** * Custom element that implements a status card for IP protection. @@ -233,20 +235,20 @@ export default class IPProtectionStatusCard extends MozLitElement { fill: "currentColor", }); - return this.location - ? html` - <div id="vpn-details"> - <div id="location-label" style=${labelStyles}> + return html` + <div id="vpn-details"> + <bandwidth-usage numeric></bandwidth-usage>${this.location + ? html`<div id="location-label" style=${labelStyles}> <span>${this.location.name}</span> <img src="chrome://global/skin/icons/info.svg" data-l10n-id="ipprotection-location-title" style=${imgStyles} /> - </div> - </div> - ` - : null; + </div>` + : null} + </div> + `; } render() { diff --git a/browser/components/ipprotection/jar.mn b/browser/components/ipprotection/jar.mn @@ -14,3 +14,5 @@ browser.jar: content/browser/ipprotection/ipprotection-site-settings-control.mjs (content/ipprotection-site-settings-control.mjs) content/browser/ipprotection/ipprotection-status-card.css (content/ipprotection-status-card.css) content/browser/ipprotection/ipprotection-status-card.mjs (content/ipprotection-status-card.mjs) + content/browser/ipprotection/bandwidth-usage.css (content/bandwidth-usage.css) + content/browser/ipprotection/bandwidth-usage.mjs (content/bandwidth-usage.mjs) diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js @@ -4,7 +4,7 @@ "use strict"; -const { LINKS } = ChromeUtils.importESModule( +const { BANDWIDTH_USAGE, LINKS } = ChromeUtils.importESModule( "chrome://browser/content/ipprotection/ipprotection-constants.mjs" ); const lazy = {}; @@ -27,6 +27,10 @@ add_task(async function test_status_card_in_panel() { code: "us", }; + await SpecialPowers.pushPrefEnv({ + set: [["browser.ipProtection.bandwidth", 50 * BANDWIDTH_USAGE.MB_TO_GB]], + }); + let content = await openPanel({ isSignedOut: false, location: mockLocation, @@ -46,17 +50,18 @@ add_task(async function test_status_card_in_panel() { "Status card connection toggle data-l10n-id should be correct by default" ); - let descriptionMetadata = statusCard?.statusGroupEl.description; + const locationEl = + statusCard.statusGroupEl.shadowRoot.querySelector("#location-label"); Assert.ok( - descriptionMetadata.values.length, - "Ensure there are elements loaded in the description slot" + BrowserTestUtils.isVisible(locationEl), + "Location element should be present and visible" ); - - let locationNameFilter = descriptionMetadata.values.filter( - locationName => locationName === mockLocation.name + Assert.equal( + locationEl.textContent.trim(), + mockLocation.name, + "Location element should be showing correct location" ); - Assert.ok(locationNameFilter.length, "Found location in status card"); // Set state as if protection is enabled await setPanelState({ @@ -75,6 +80,23 @@ add_task(async function test_status_card_in_panel() { "Status card connection toggle data-l10n-id should be correct when protection is enabled" ); + const bandwidthEl = + statusCard.statusGroupEl.shadowRoot.querySelector("bandwidth-usage"); + Assert.ok( + BrowserTestUtils.isVisible(bandwidthEl), + "bandwidth-usage should be present and visible" + ); + Assert.equal( + bandwidthEl.bandwidthUsedGB, + 50, + "Bandwidth should have 50 GB used" + ); + Assert.equal( + bandwidthEl.bandwidthUsageLeftGB, + 100, + "Bandwidth should have 100 GB left" + ); + await closePanel(); }); diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_usage.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_usage.js @@ -44,3 +44,39 @@ add_task(async function test_createConnection_and_proxy() { ); }); }); + +add_task(async function test_bandwidthUsage() { + await withProxyServer(async proxyInfo => { + // Create the IPP connection filter + const filter = IPPChannelFilter.create(); + filter.initialize("", proxyInfo.server); + filter.start(); + + const usage = new IPProtectionUsage(); + usage.addIsolationKey(filter.isolationKey); + usage.start(); + + // Make the body size 1 MB so the pref is set + let channel = makeChannel( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + `http://example.com/`, + "POST", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed elementum aliquet metus sed vestibulum.".repeat( + 10000 + ) + ); + await promiseChannelDone(channel); + + usage.stop(); + + await TestUtils.waitForCondition( + () => Services.prefs.getIntPref("browser.ipProtection.bandwidth", 0) > 0, + "Waiting for usage write" + ); + Assert.greater( + Services.prefs.getIntPref("browser.ipProtection.bandwidth", 0), + 0, + "Bandwidth usage should have recorded at least 1 MB" + ); + }); +}); diff --git a/browser/components/ipprotection/tests/browser/head.js b/browser/components/ipprotection/tests/browser/head.js @@ -294,6 +294,7 @@ add_setup(async function setupVPN() { Services.prefs.clearUserPref("browser.ipProtection.entitlementCache"); Services.prefs.clearUserPref("browser.ipProtection.locationListCache"); Services.prefs.clearUserPref("browser.ipProtection.onboardingMessageMask"); + Services.prefs.clearUserPref("browser.ipProtection.bandwidth"); }); }); @@ -434,3 +435,95 @@ async function putServerInRemoteSettings( } } /* exported putServerInRemoteSettings */ + +/** + * Creates a new channel for the given URI. + * + * @param {*} aUri the URI to create the channel for. + * @param {*} method the HTTP method to use (default: "GET"). + * @param {*} body the request body (for POST requests). + * @param {*} proxyInfo proxy information (if any) makes this channel a proxied channel. + * @returns {nsIHttpChannel | nsIProxiedChannel} + */ +function makeChannel(aUri, method = "GET", body = null, proxyInfo = null) { + let channel; + if (proxyInfo) { + let httpHandler = Services.io.getProtocolHandler("http"); + httpHandler.QueryInterface(Ci.nsIProxiedProtocolHandler); + let uri = Services.io.newURI(aUri); + + let { loadInfo } = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + + channel = httpHandler.newProxiedChannel( + uri, + proxyInfo, + 0, // proxy resolve flags + null, // proxy resolve URI + loadInfo + ); + } else { + channel = NetUtil.newChannel({ + uri: aUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + channel.requestMethod = method; + } + + if (body) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setUTF8Data(body); + channel + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(stream, "text/plain", body.length); + } + return channel; +} + +function promiseChannelDone(chan) { + return new Promise((resolve, reject) => { + chan.asyncOpen(new ChannelListener(resolve, reject)); + }); +} + +/** + * Mocks a channel listener. + */ +class ChannelListener { + constructor(resolve, reject) { + this.resolve = resolve; + this.reject = reject; + } + onStartRequest() {} + onDataAvailable() {} + onStopRequest() { + this.resolve(); + } +} + +/** + * • Creates a profile dir & initialises FOG. + * • Resets/flushes metrics so each test starts clean. + * • Spins‑up an HttpServer, hands its URL to the test body, then stops it. + * + * @param {string} path Path for the single route, e.g. "/get". + * @param {Function} handler httpd.js style path handler. + * @param {Function} testBody async fn(url:string):void – the real test. + */ +async function withSetup(path, handler, testBody) { + let server = new HttpServer(); + server.registerPathHandler(path, handler); + server.start(-1); + let port = server.identity.primaryPort; + let url = `http://localhost:${port}${path}`; + + try { + await testBody(url); + } finally { + await new Promise(r => server.stop(r)); + } +} diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProtectionUsage.js b/browser/components/ipprotection/tests/xpcshell/test_IPProtectionUsage.js @@ -130,10 +130,12 @@ add_task(async function test_countChannel_get() { resp.write("hello world"); }, async url => { + const usage = new IPProtectionUsage(); + let channel = makeChannel(url, "GET"); await promiseChannelDone(channel); - IPProtectionUsage.countChannel(channel); + usage.countChannel(channel); Assert.greater( Glean.ipprotection.usageRx.testGetValue().sum, @@ -166,10 +168,12 @@ add_task(async function test_countChannel_post() { resp.write("posted!"); }, async url => { + const usage = new IPProtectionUsage(); + let channel = makeChannel(url, "POST", "some data"); await promiseChannelDone(channel); - IPProtectionUsage.countChannel(channel); + usage.countChannel(channel); Assert.greater( Glean.ipprotection.usageRx.testGetValue().sum, @@ -194,10 +198,12 @@ add_task(async function test_countChannel_cache() { resp.write("cached response"); }, async url => { + const usage = new IPProtectionUsage(); + let channel = makeChannel(url, "GET"); await promiseChannelDone(channel); - IPProtectionUsage.countChannel(channel); + usage.countChannel(channel); const afterRx = Glean.ipprotection.usageRx.testGetValue().sum; Assert.greater( @@ -209,7 +215,7 @@ add_task(async function test_countChannel_cache() { let channel2 = makeChannel(url, "GET"); await promiseChannelDone(channel2); - IPProtectionUsage.countChannel(channel2); + usage.countChannel(channel2); Assert.equal( afterRx, diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js @@ -2873,6 +2873,10 @@ SettingGroupManager.registerGroups({ ], }, { + id: "ipProtectionBandwidth", + control: "bandwidth-usage", + }, + { id: "ipProtectionAdditionalLinks", control: "moz-box-group", options: [ diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml @@ -97,6 +97,7 @@ <script type="module" src="chrome://global/content/elements/moz-input-color.mjs"></script> <script type="module" src="chrome://browser/content/preferences/widgets/sync-device-name.mjs"></script> <script type="module" src="chrome://browser/content/preferences/widgets/sync-engines-list.mjs"></script> + <script type="module" src="chrome://browser/content/ipprotection/bandwidth-usage.mjs"></script> </head> <html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js @@ -1503,6 +1503,11 @@ Preferences.addSetting({ visible: ({ ipProtectionVisible }) => ipProtectionVisible.value, }); Preferences.addSetting({ + id: "ipProtectionBandwidth", + deps: ["ipProtectionVisible"], + visible: ({ ipProtectionVisible }) => ipProtectionVisible.value, +}); +Preferences.addSetting({ id: "ipProtectionAdditionalLinks", deps: ["ipProtectionVisible"], visible: ({ ipProtectionVisible }) => ipProtectionVisible.value, diff --git a/browser/locales-preview/ipProtection.ftl b/browser/locales-preview/ipProtection.ftl @@ -133,4 +133,28 @@ ip-protection-exclusions-desc = Use VPN for all websites except ones on this lis ipprotection-site-settings-title = .title = VPN site settings +## IP Proctection Bandwidth + +ip-protection-bandwidth-header = Monthly VPN data + +## Variables +## $usageLeft (number) - The amount of data a user has left in a month (in GB) +## $maxUsage (number) - The maximum amount of data a user can use in a month (in GB) + +ip-protection-bandwidth-left-this-month-gb = { $usageLeft } GB of { $maxUsage } GB left this month +ip-protection-bandwidth-left-gb = { $usageLeft } GB of { $maxUsage } GB left + +## Variables +## $usageLeft (number) - The amount of data a user has left in a month (in MB) +## $maxUsage (number) - The maximum amount of data a user can use in a month (in GB) + +ip-protection-bandwidth-left-this-month-mb = { $usageLeft } MB of { $maxUsage } GB left this month +ip-protection-bandwidth-left-mb = { $usageLeft } MB of { $maxUsage } GB left + +## Variables +## $maxUsage (number) - The maximum amount of data a user can use in a month (in GB) + +ip-protection-bandwidth-hit-for-the-month = You’ve used all { $maxUsage } GB of your VPN data. Access will reset next month. +ip-protection-bandwidth-help-text = Resets to { $maxUsage } GB on the first of every month. + ##