Bug 1901533 - Moves Refresh Firefox Infobar into Messaging System r=firefox-desktop-core-reviewers ,fluent-reviewers,omc-reviewers,bolsson,mossop,sachung

Scope of this patch is to pull the refresh (unused and reinstall) firefox infobar into messaging system.

The infobars are now idle dispatched through the defaultBrowserCheck trigger (previously `onFirstWindowLoaded`), and will respect [activeNotifications](https://searchfox.org/firefox-main/source/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs#1125)

 See screenshots below:

{F79391908}

{F79391907}

{F79391906}

[try run](https://treeherder.mozilla.org/jobs?repo=try&landoInstance=lando-prod-2025&landoCommitID=88932)

Differential Revision: https://phabricator.services.mozilla.com/D323372
This commit is contained in:
mimi
2026-09-11 02:52:54 +00:00
committed by mjung@mozilla.com
parent c2568070ec
commit 68b523fa01
17 changed files with 365 additions and 135 deletions
-120
View File
@@ -78,7 +78,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
WebProtocolHandlerRegistrar:
"resource:///modules/WebProtocolHandlerRegistrar.sys.mjs",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
@@ -509,73 +508,6 @@ BrowserGlue.prototype = {
}
},
/**
* Show a notification bar offering a reset.
*
* @param reason
* String of either "unused" or "uninstall", specifying the reason
* why a profile reset is offered.
*/
_resetProfileNotification(reason) {
let win = lazy.BrowserWindowTracker.getTopWindow({
allowFromInactiveWorkspace: true,
});
if (!win) {
return;
}
const { ResetProfile } = ChromeUtils.importESModule(
"resource://gre/modules/ResetProfile.sys.mjs"
);
if (!ResetProfile.resetSupported()) {
return;
}
let productName = lazy.gBrandBundle.GetStringFromName("brandShortName");
let resetBundle = Services.strings.createBundle(
"chrome://global/locale/resetProfile.properties"
);
let message;
if (reason == "unused") {
message = resetBundle.formatStringFromName("resetUnusedProfile.message", [
productName,
]);
} else if (reason == "uninstall") {
message = resetBundle.formatStringFromName("resetUninstalled.message", [
productName,
]);
} else {
throw new Error(
`Unknown reason (${reason}) given to _resetProfileNotification.`
);
}
let buttons = [
{
label: resetBundle.formatStringFromName(
"refreshProfile.resetButton.label",
[productName]
),
accessKey: resetBundle.GetStringFromName(
"refreshProfile.resetButton.accesskey"
),
callback() {
ResetProfile.openConfirmationDialog(win);
},
},
];
win.gNotificationBox.appendNotification(
"reset-profile-notification",
{
label: message,
image: "chrome://global/skin/icons/question-64.png",
priority: win.gNotificationBox.PRIORITY_INFO_LOW,
},
buttons
);
},
_notifyUnsignedAddonsDisabled() {
let win = lazy.BrowserWindowTracker.getTopWindow({
allowFromInactiveWorkspace: true,
@@ -802,8 +734,6 @@ BrowserGlue.prototype = {
}
});
this._maybeOfferProfileReset();
this._checkForOldBuildUpdates();
// Check if Sync is configured
@@ -822,56 +752,6 @@ BrowserGlue.prototype = {
this._firstWindowTelemetry(aWindow);
},
_maybeOfferProfileReset() {
// Offer to reset a user's profile if it hasn't been used for 60 days.
const OFFER_PROFILE_RESET_INTERVAL_MS = 60 * 24 * 60 * 60 * 1000;
let lastUse = Services.appinfo.replacedLockTime;
let disableResetPrompt = Services.prefs.getBoolPref(
"browser.disableResetPrompt",
false
);
// Also check prefs.js last modified timestamp as a backstop.
// This helps for cases where the lock file checks don't work,
// e.g. NFS or because the previous time Firefox ran, it ran
// for a very long time. See bug 1054947 and related bugs.
lastUse = Math.max(
lastUse,
Services.prefs.userPrefsFileLastModifiedAtStartup
);
if (
!disableResetPrompt &&
lastUse &&
Date.now() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS
) {
this._resetProfileNotification("unused");
} else if (AppConstants.platform == "win" && !disableResetPrompt) {
// Check if we were just re-installed and offer Firefox Reset
let updateChannel;
try {
updateChannel = ChromeUtils.importESModule(
"resource://gre/modules/UpdateUtils.sys.mjs"
).UpdateUtils.UpdateChannel;
} catch (ex) {}
if (updateChannel) {
let uninstalledValue = lazy.WindowsRegistry.readRegKey(
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
"Software\\Mozilla\\Firefox",
`Uninstalled-${updateChannel}`
);
let removalSuccessful = lazy.WindowsRegistry.removeRegKey(
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
"Software\\Mozilla\\Firefox",
`Uninstalled-${updateChannel}`
);
if (removalSuccessful && uninstalledValue == "True") {
this._resetProfileNotification("uninstall");
}
}
}
},
/**
* Application shutdown handler.
*
+57
View File
@@ -0,0 +1,57 @@
/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
});
// Reads and clears the registry value the Windows uninstaller writes
function readAndClearUninstalledValue() {
if (AppConstants.platform != "win") {
return false;
}
if (Services.prefs.getBoolPref("browser.disableResetPrompt", false)) {
return false;
}
let updateChannel;
try {
updateChannel = ChromeUtils.importESModule(
"resource://gre/modules/UpdateUtils.sys.mjs"
).UpdateUtils.UpdateChannel;
} catch (ex) {}
if (updateChannel) {
let uninstalledValue = lazy.WindowsRegistry.readRegKey(
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
"Software\\Mozilla\\Firefox",
`Uninstalled-${updateChannel}`
);
let removalSuccessful = lazy.WindowsRegistry.removeRegKey(
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
"Software\\Mozilla\\Firefox",
`Uninstalled-${updateChannel}`
);
return removalSuccessful && uninstalledValue == "True";
}
return false;
}
export const ReinstallCheck = {
_wasReinstalled: null,
/**
* @returns {boolean} whether Firefox was reinstalled since the previous run.
*/
get wasReinstalled() {
if (this._wasReinstalled === null) {
this._wasReinstalled = readAndClearUninstalledValue();
}
return this._wasReinstalled;
},
};
@@ -472,6 +472,25 @@ declare const profileAgeReset: undefined | UnixEpochNumber;
type UnixEpochNumber = number;
```
### `profileLastUse`
The date the profile was last used before the current session, as a UNIX Epoch
timestamp. This is the more recent of the previous session's lock file time and
the `prefs.js` modification time, and is `0` when neither is available.
#### Examples
* Has the profile been unused for at least 60 days?
```java
profileLastUse && currentDate|date - profileLastUse >= 5184000000
```
#### Definition
```ts
declare const profileLastUse: UnixEpochNumber;
// UnixEpochNumber is UNIX Epoch timestamp, e.g. 1522843725924
type UnixEpochNumber = number;
```
### `providerCohorts`
Information about cohort settings (from prefs, including shield studies) for each provider.
@@ -1334,6 +1353,16 @@ restore the previous session on startup; `false` otherwise.
A boolean. `true` when both the current install and current profile support creating additional profiles using the `SelectableProfileService`; `false` otherwise.
### `canResetProfile`
A boolean. `true` when the current profile can be refreshed.
Any message using the `RESET_PROFILE` action should include this in its targeting.
### `isFirefoxReinstalled`
Windows-only. A boolean. `true` when Firefox was uninstalled and then reinstalled over an existing profile since the previous run; `false` otherwise.
### `hasSelectableProfiles`
A boolean. `true` when the `toolkit.profiles.storeID` pref has a value. Indicates that the profile is part of a profile group managed by the `SelectableProfileService`, and the user has used the multiple profiles feature. `false` otherwise.
@@ -76,6 +76,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
ReinstallCheck: "moz-src:///browser/components/ReinstallCheck.sys.mjs",
ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
SearchService: "moz-src:///toolkit/components/search/SearchService.sys.mjs",
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
SelectableProfileService:
@@ -785,6 +787,22 @@ const TargetingGetters = {
get profileAgeReset() {
return lazy.ProfileAge().then(times => times.reset);
},
get profileLastUse() {
// The lock file records when the profile was last used, but it can be
// unreliable, e.g. on NFS or when the previous session ran for a very long
// time. Use the prefs.js modification time as a backstop. See bug 1054947
// and related bugs.
return Math.max(
Services.appinfo.replacedLockTime,
Services.prefs.userPrefsFileLastModifiedAtStartup
);
},
get canResetProfile() {
return lazy.ResetProfile.resetSupported();
},
get isFirefoxReinstalled() {
return lazy.ReinstallCheck.wasReinstalled;
},
get usesFirefoxSync() {
return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
},
@@ -2625,6 +2625,49 @@ const BASE_MESSAGES = () => [
id: "selectableProfilesUpdated",
},
},
{
id: "REFRESH_UNUSED_PROFILE_INFOBAR",
template: "infobar",
content: {
type: "global",
priority: 1,
text: { string_id: "refresh-unused-profile-infobar-message" },
buttons: [
{
label: { string_id: "refresh-profile-infobar-button" },
action: { type: "RESET_PROFILE" },
},
],
},
trigger: {
id: "defaultBrowserCheck",
},
skip_in_tests: "fires on startup, may interfere with other tests",
priority: 1,
targeting:
"source == 'startup' && canResetProfile && !'browser.disableResetPrompt'|preferenceValue && profileLastUse && currentDate|date - profileLastUse >= 5184000000 && !activeNotifications",
},
{
id: "REFRESH_REINSTALLED_PROFILE_INFOBAR",
template: "infobar",
content: {
type: "global",
priority: 1,
text: { string_id: "refresh-reinstalled-profile-infobar-message" },
buttons: [
{
label: { string_id: "refresh-profile-infobar-button" },
action: { type: "RESET_PROFILE" },
},
],
},
trigger: {
id: "defaultBrowserCheck",
},
skip_in_tests: "fires on startup, may interfere with other tests",
targeting:
"source == 'startup' && isFirefoxReinstalled && canResetProfile && !'browser.disableResetPrompt'|preferenceValue && !activeNotifications",
},
{
id: "updated-privacy-notice-notification-infobar",
groups: ["cfr"],
@@ -27,6 +27,8 @@ ChromeUtils.defineESModuleGetters(this, {
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
QueryCache: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
ReinstallCheck: "moz-src:///browser/components/ReinstallCheck.sys.mjs",
ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
SearchService: "moz-src:///toolkit/components/search/SearchService.sys.mjs",
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
@@ -357,6 +359,83 @@ add_task(async function check_canCreateSelectableProfiles() {
await SpecialPowers.popPrefEnv();
});
add_task(async function check_canResetProfile() {
const sandbox = sinon.createSandbox();
const resetSupported = sandbox.stub(ResetProfile, "resetSupported");
resetSupported.returns(true);
is(
await ASRouterTargeting.Environment.canResetProfile,
true,
"should be true when the profile supports being reset"
);
const message = { id: "foo", targeting: "canResetProfile" };
is(
await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
message,
"should select the right item by canResetProfile"
);
resetSupported.returns(false);
is(
await ASRouterTargeting.Environment.canResetProfile,
false,
"should be false when the profile doesn't support being reset"
);
sandbox.restore();
});
add_task(async function check_profileLastUse() {
is(
await ASRouterTargeting.Environment.profileLastUse,
Math.max(
Services.appinfo.replacedLockTime,
Services.prefs.userPrefsFileLastModifiedAtStartup
),
"should be the most recent lock file and prefs.js timestamps"
);
const message = {
id: "foo",
targeting: "profileLastUse <= currentDate|date",
};
is(
await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
message,
"should select correct item by profileLastUse"
);
});
add_task(async function check_isFirefoxReinstalled() {
const sandbox = sinon.createSandbox();
const wasReinstalled = sandbox.stub(ReinstallCheck, "wasReinstalled");
wasReinstalled.get(() => true);
is(
await ASRouterTargeting.Environment.isFirefoxReinstalled,
true,
"should be true when a reinstall was detected"
);
const message = { id: "foo", targeting: "isFirefoxReinstalled" };
is(
await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
message,
"should select correct item by isFirefoxReinstalled"
);
wasReinstalled.get(() => false);
is(
await ASRouterTargeting.Environment.isFirefoxReinstalled,
false,
"should be false when no reinstall was detected"
);
sandbox.restore();
});
add_task(async function check_hasSelectableProfiles() {
is(
await ASRouterTargeting.Environment.hasSelectableProfiles,
+1
View File
@@ -124,6 +124,7 @@ MOZ_SRC_FILES += [
"DefaultBrowserCheck.sys.mjs",
"DesktopActorRegistry.sys.mjs",
"ProfileDataUpgrader.sys.mjs",
"ReinstallCheck.sys.mjs",
"StartupTelemetry.sys.mjs",
]
@@ -497,3 +497,14 @@ lapsed-user-toast-title = { -brand-product-name } still has your back
lapsed-user-toast-subtitle = Check out new ways you can browse with more choice, privacy, and control.
lapsed-user-toast-whats-new-button = See whats new
lapsed-user-toast-dismiss-button = Dismiss
## Refresh Firefox infobar
##
## Shown at startup when the profile has not been used in over 60 days, or when
## Firefox has just been reinstalled over an existing profile.
## Both offer to reset the profile to a fresh state.
refresh-unused-profile-infobar-message = It looks like you havent started { -brand-short-name } in a while. Do you want to clean it up for a fresh, like-new experience? And by the way, welcome back!
refresh-reinstalled-profile-infobar-message = Looks like youve reinstalled { -brand-short-name }. Want us to clean it up for a fresh, like-new experience?
refresh-profile-infobar-button = Refresh { -brand-short-name }…
.accesskey = e
@@ -0,0 +1,50 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
import fluent.syntax.ast as FTL
from fluent.migrate import COPY, REPLACE
from fluent.migrate.helpers import TERM_REFERENCE
def migrate(ctx):
"""Bug 1901533 - Move the Refresh Firefox infobars into the messaging system, part {index}."""
source = "toolkit/chrome/global/resetProfile.properties"
target = "browser/browser/newtab/asrouter.ftl"
ctx.add_transforms(
target,
target,
[
FTL.Message(
id=FTL.Identifier("refresh-unused-profile-infobar-message"),
value=REPLACE(
source,
"resetUnusedProfile.message",
{"%1$S": TERM_REFERENCE("brand-short-name")},
),
),
FTL.Message(
id=FTL.Identifier("refresh-reinstalled-profile-infobar-message"),
value=REPLACE(
source,
"resetUninstalled.message",
{"%1$S": TERM_REFERENCE("brand-short-name")},
),
),
FTL.Message(
id=FTL.Identifier("refresh-profile-infobar-button"),
value=REPLACE(
source,
"refreshProfile.resetButton.label",
{"%1$S": TERM_REFERENCE("brand-short-name")},
),
attributes=[
FTL.Attribute(
id=FTL.Identifier("accesskey"),
value=COPY(source, "refreshProfile.resetButton.accesskey"),
)
],
),
],
)
@@ -66,6 +66,9 @@ export const BLOCKED_ACTION_ONLY_ACTIONS = new Set([
"IPPROTECTION_ENROLL",
"CREATE_NEW_SELECTABLE_PROFILE",
// Offers to erase the user's profile data and restart the browser.
"RESET_PROFILE",
// Sends data off the device, or fabricates a record that the user consented
// to something.
"SUMMARIZE_PAGE",
@@ -63,6 +63,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
Referrals: "resource:///modules/referrals/Referrals.sys.mjs",
ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
@@ -1220,6 +1221,13 @@ export const SpecialMessageActions = {
);
break;
}
case "RESET_PROFILE": {
if (!lazy.ResetProfile.resetSupported()) {
throw new Error("Profile reset is not supported for this profile.");
}
await lazy.ResetProfile.openConfirmationDialog(window);
break;
}
default:
throw new Error(
`Special message action with type ${action.type} is unsupported.`
@@ -879,6 +879,18 @@
"additionalProperties": false,
"description": "Creates a new user profile using SelectableProfileService and launches it in a new instance"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["RESET_PROFILE"]
}
},
"required": ["type"],
"additionalProperties": false,
"description": "Opens the refresh confirmation dialog, which resets the current profile and restarts the browser"
},
{
"type": "object",
"properties": {
@@ -545,6 +545,14 @@ Any message that uses this action should have `canCreateSelectableProfiles` as p
- args: (none)
### `RESET_PROFILE`
Opens the refresh confirmation dialog, which resets the current profile and restarts the browser
Any message that uses this action should have `canResetProfile` as part of the targeting, to ensure we don't show a message where the action will not work.
- args: (none)
### `SUBMIT_ONBOARDING_OPT_OUT_PING`
Submits a Glean `onboarding-opt-out` ping. Should only be used during preonboarding (but this is not enforced).
@@ -88,6 +88,8 @@ run-if = [
"os == 'linux'",
]
["browser_sma_reset_profile.js"]
["browser_sma_restore_session.js"]
["browser_sma_set_bookmarks_toolbar_visibility.js"]
@@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { ResetProfile } = ChromeUtils.importESModule(
"resource://gre/modules/ResetProfile.sys.mjs"
);
add_task(async function test_RESET_PROFILE() {
let sandbox = sinon.createSandbox();
sandbox.stub(ResetProfile, "resetSupported").returns(true);
let dialogStub = sandbox.stub(ResetProfile, "openConfirmationDialog");
await SMATestUtils.executeAndValidateAction({ type: "RESET_PROFILE" });
Assert.equal(
dialogStub.callCount,
1,
"openConfirmationDialog called by the action"
);
sandbox.restore();
});
add_task(async function test_RESET_PROFILE_unsupported() {
let sandbox = sinon.createSandbox();
sandbox.stub(ResetProfile, "resetSupported").returns(false);
let dialogStub = sandbox.stub(ResetProfile, "openConfirmationDialog");
await Assert.rejects(
SpecialMessageActions.handleAction({ type: "RESET_PROFILE" }, gBrowser),
/Profile reset is not supported/,
"should reject when reset is not supported"
);
Assert.equal(
dialogStub.callCount,
0,
"openConfirmationDialog not called when reset is unsupported"
);
sandbox.restore();
});
@@ -1,14 +0,0 @@
# 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/.
# LOCALIZATION NOTE: These strings are used for profile reset.
# LOCALIZATION NOTE (resetUnusedProfile.message): %S is brandShortName.
resetUnusedProfile.message=It looks like you havent started %S in a while. Do you want to clean it up for a fresh, like-new experience? And by the way, welcome back!
# LOCALIZATION NOTE (resetUninstalled.message): %S is brandShortName.
resetUninstalled.message=Looks like youve reinstalled %S. Want us to clean it up for a fresh, like-new experience?
# LOCALIZATION NOTE (refreshProfile.resetButton.label): %S is brandShortName.
refreshProfile.resetButton.label=Refresh %S…
refreshProfile.resetButton.accesskey=e
-1
View File
@@ -33,7 +33,6 @@
#if !defined(XP_WIN) && !defined(MOZ_GECKOVIEW)
locale/@AB_CD@/global/printdialog.properties (%chrome/global/printdialog.properties)
#endif
locale/@AB_CD@/global/resetProfile.properties (%chrome/global/resetProfile.properties)
locale/@AB_CD@/global/dialog.properties (%chrome/global/dialog.properties)
locale/@AB_CD@/global/viewSource.properties (%chrome/global/viewSource.properties)
locale/@AB_CD@/global/wizard.properties (%chrome/global/wizard.properties)