mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 04:41:11 +00:00
Bug 1923868 - Support setting Firefox as default based on installer attribution campaign r=firefox-desktop-core-reviewers ,nalexander,omc-reviewers,pdahiya
This patch adds an startup idle task that sets the browser as default if an attribution campaign id of "set_default_browser" is present on first run. This works supports an upcoming experiment where users will have the option to "download as default" via [[ https://www.mozilla.org/en-US/firefox/new/ | the stub installer marketing page ]]. Differential Revision: https://phabricator.services.mozilla.com/D225212
This commit is contained in:
parent
858d3f15e6
commit
22cb24bd81
@ -137,6 +137,9 @@ export class AboutWelcomeChild extends JSWindowActorChild {
|
||||
Cu.exportFunction(this.AWNewScreen.bind(this), window, {
|
||||
defineAs: "AWNewScreen",
|
||||
});
|
||||
Cu.exportFunction(this.AWGetUnhandledCampaignAction.bind(this), window, {
|
||||
defineAs: "AWGetUnhandledCampaignAction",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,6 +392,12 @@ export class AboutWelcomeChild extends JSWindowActorChild {
|
||||
return this.wrapPromise(this.sendQuery("AWPage:NEW_SCREEN", screenId));
|
||||
}
|
||||
|
||||
AWGetUnhandledCampaignAction() {
|
||||
return this.sendQueryAndCloneForContent(
|
||||
"AWPage:GET_UNHANDLED_CAMPAIGN_ACTION"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{type: string, detail?: any}} event
|
||||
* @override
|
||||
|
@ -34,6 +34,8 @@ ChromeUtils.defineLazyGetter(
|
||||
);
|
||||
|
||||
const DID_SEE_ABOUT_WELCOME_PREF = "trailhead.firstrun.didSeeAboutWelcome";
|
||||
const DID_HANDLE_CAMAPAIGN_ACTION_PREF =
|
||||
"trailhead.firstrun.didHandleCampaignAction";
|
||||
const AWTerminate = {
|
||||
WINDOW_CLOSED: "welcome-window-closed",
|
||||
TAB_CLOSED: "welcome-tab-closed",
|
||||
@ -260,6 +262,28 @@ export class AboutWelcomeParent extends JSWindowActorParent {
|
||||
case "AWPage:SEND_TO_DEVICE_EMAILS_SUPPORTED": {
|
||||
return lazy.BrowserUtils.sendToDeviceEmailsSupported();
|
||||
}
|
||||
case "AWPage:GET_UNHANDLED_CAMPAIGN_ACTION": {
|
||||
if (
|
||||
!Services.prefs.getBoolPref(DID_HANDLE_CAMAPAIGN_ACTION_PREF, false)
|
||||
) {
|
||||
return lazy.AWScreenUtils.getUnhandledCampaignAction();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "AWPage:HANDLE_CAMPAIGN_ACTION": {
|
||||
if (
|
||||
!Services.prefs.getBoolPref(DID_HANDLE_CAMAPAIGN_ACTION_PREF, false)
|
||||
) {
|
||||
lazy.SpecialMessageActions.handleAction({ type: data }, browser);
|
||||
try {
|
||||
Services.prefs.setBoolPref(DID_HANDLE_CAMAPAIGN_ACTION_PREF, true);
|
||||
} catch (e) {
|
||||
lazy.log.debug(`Fails to set ${DID_HANDLE_CAMAPAIGN_ACTION_PREF}.`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
lazy.log.debug(`Unexpected event ${type} was not handled.`);
|
||||
}
|
||||
|
@ -59,6 +59,23 @@ export const MultiStageAboutWelcome = props => {
|
||||
|
||||
didFilter.current = true;
|
||||
|
||||
// After completing screen filtering, trigger any unhandled campaign
|
||||
// action present in the attribution campaign data. This updates the
|
||||
// "trailhead.firstrun.didHandleCampaignAction" preference, marking the
|
||||
// actions as complete to prevent them from being handled on subsequent
|
||||
// visits to about:welcome. Do not await getting the action to avoid
|
||||
// blocking the thread.
|
||||
window
|
||||
.AWGetUnhandledCampaignAction?.()
|
||||
.then(action => {
|
||||
if (typeof action === "string") {
|
||||
AboutWelcomeUtils.handleCampaignAction(action, props.message_id);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Failed to get unhandled campaign action:", error);
|
||||
});
|
||||
|
||||
const screenInitials = filteredScreens
|
||||
.map(({ id }) => id?.split("_")[1]?.[0])
|
||||
.join("");
|
||||
|
@ -70,6 +70,13 @@ export const AboutWelcomeUtils = {
|
||||
getLoadingStrategyFor(url) {
|
||||
return url?.startsWith("http") ? "lazy" : "eager";
|
||||
},
|
||||
handleCampaignAction(action, messageId) {
|
||||
window.AWSendToParent("HANDLE_CAMPAIGN_ACTION", action).then(handled => {
|
||||
if (handled) {
|
||||
this.sendActionTelemetry(messageId, "CAMPAIGN_ACTION");
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_RTAMO_CONTENT = {
|
||||
|
@ -100,6 +100,13 @@ const AboutWelcomeUtils = {
|
||||
getLoadingStrategyFor(url) {
|
||||
return url?.startsWith("http") ? "lazy" : "eager";
|
||||
},
|
||||
handleCampaignAction(action, messageId) {
|
||||
window.AWSendToParent("HANDLE_CAMPAIGN_ACTION", action).then(handled => {
|
||||
if (handled) {
|
||||
this.sendActionTelemetry(messageId, "CAMPAIGN_ACTION");
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_RTAMO_CONTENT = {
|
||||
@ -215,6 +222,20 @@ const MultiStageAboutWelcome = props => {
|
||||
// e.g. if AW_LANGUAGE_MISMATCH exists, use it from existing screens
|
||||
setScreens(filteredScreens.map(filtered => screens.find(s => s.id === filtered.id) ?? filtered));
|
||||
didFilter.current = true;
|
||||
|
||||
// After completing screen filtering, trigger any unhandled campaign
|
||||
// action present in the attribution campaign data. This updates the
|
||||
// "trailhead.firstrun.didHandleCampaignAction" preference, marking the
|
||||
// actions as complete to prevent them from being handled on subsequent
|
||||
// visits to about:welcome. Do not await getting the action to avoid
|
||||
// blocking the thread.
|
||||
window.AWGetUnhandledCampaignAction?.().then(action => {
|
||||
if (typeof action === "string") {
|
||||
_lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleCampaignAction(action, props.message_id);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Failed to get unhandled campaign action:", error);
|
||||
});
|
||||
const screenInitials = filteredScreens.map(({
|
||||
id
|
||||
}) => id?.split("_")[1]?.[0]).join("");
|
||||
|
@ -42,6 +42,20 @@ export const AWScreenUtils = {
|
||||
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Returns the string identifier of an unhandled campaign action, if
|
||||
* applicable otherwise false.
|
||||
*
|
||||
* @returns {string|boolean}
|
||||
*/
|
||||
async getUnhandledCampaignAction() {
|
||||
const UNHANDLED_CAMPAIGN_ACTION_TARGETING = "unhandledCampaignAction";
|
||||
let result = await lazy.ASRouter.evaluateExpression({
|
||||
expression: UNHANDLED_CAMPAIGN_ACTION_TARGETING,
|
||||
context: lazy.ASRouterTargeting.Environment,
|
||||
});
|
||||
return result?.evaluationStatus?.result || false;
|
||||
},
|
||||
/**
|
||||
* Filter out screens whose targeting do not match.
|
||||
*
|
||||
|
@ -90,7 +90,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
|
||||
{
|
||||
id: "AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN",
|
||||
targeting:
|
||||
"doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
|
||||
"doesAppNeedPin && (unhandledCampaignAction != 'SET_DEFAULT_BROWSER') && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
|
||||
content: {
|
||||
position: "split",
|
||||
split_narrow_bkg_position: "-60px",
|
||||
@ -224,7 +224,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
|
||||
{
|
||||
id: "AW_EASY_SETUP_NEEDS_DEFAULT",
|
||||
targeting:
|
||||
"!doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
|
||||
"!doesAppNeedPin && (unhandledCampaignAction != 'SET_DEFAULT_BROWSER') && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
|
||||
content: {
|
||||
position: "split",
|
||||
split_narrow_bkg_position: "-60px",
|
||||
@ -335,7 +335,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
|
||||
{
|
||||
id: "AW_EASY_SETUP_NEEDS_PIN",
|
||||
targeting:
|
||||
"doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)",
|
||||
"doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser || (unhandledCampaignAction == 'SET_DEFAULT_BROWSER'))",
|
||||
content: {
|
||||
position: "split",
|
||||
split_narrow_bkg_position: "-60px",
|
||||
@ -457,7 +457,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
|
||||
{
|
||||
id: "AW_EASY_SETUP_ONLY_IMPORT",
|
||||
targeting:
|
||||
"!doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)",
|
||||
"!doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser || (unhandledCampaignAction == 'SET_DEFAULT_BROWSER'))",
|
||||
content: {
|
||||
position: "split",
|
||||
split_narrow_bkg_position: "-60px",
|
||||
|
@ -14,6 +14,13 @@ run-if = [
|
||||
]
|
||||
skip-if = ["os == 'win' && msix"] # These tests rely on the ability to write postSigningData, which we can't do in MSIX builds. https://bugzilla.mozilla.org/show_bug.cgi?id=1805911
|
||||
|
||||
["browser_aboutwelcome_campaign_actions.js"]
|
||||
run-if = [
|
||||
"os == 'win'", # installation attribution is only available on Windows and macOS
|
||||
"os == 'mac'", # installation attribution is only available on Windows and macOS
|
||||
]
|
||||
skip-if = ["os == 'win' && msix"] # Attribution code cannot be written for MSIX builds
|
||||
|
||||
["browser_aboutwelcome_configurable_ui.js"]
|
||||
|
||||
["browser_aboutwelcome_fxa_signin_flow.js"]
|
||||
|
@ -0,0 +1,96 @@
|
||||
"use strict";
|
||||
|
||||
const { ASRouter } = ChromeUtils.importESModule(
|
||||
"resource:///modules/asrouter/ASRouter.sys.mjs"
|
||||
);
|
||||
const { AttributionCode } = ChromeUtils.importESModule(
|
||||
"resource:///modules/AttributionCode.sys.mjs"
|
||||
);
|
||||
const { SpecialMessageActions } = ChromeUtils.importESModule(
|
||||
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
|
||||
);
|
||||
|
||||
const TEST_ATTRIBUTION_DATA = {
|
||||
campaign: "set_default_browser",
|
||||
};
|
||||
|
||||
const DID_HANDLE_CAMAPAIGN_ACTION_PREF =
|
||||
"trailhead.firstrun.didHandleCampaignAction";
|
||||
|
||||
const TEST_PROTON_CONTENT = [
|
||||
{
|
||||
id: "AW_STEP1",
|
||||
content: {
|
||||
title: "Step 1",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
add_task(async function test_unhandled_campaign_action() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const handleActionStub = sandbox
|
||||
.stub(SpecialMessageActions, "handleAction")
|
||||
.resolves();
|
||||
|
||||
await AttributionCode.deleteFileAsync();
|
||||
await ASRouter.forceAttribution(TEST_ATTRIBUTION_DATA);
|
||||
const TEST_PROTON_JSON = JSON.stringify(TEST_PROTON_CONTENT);
|
||||
|
||||
await setAboutWelcomePref(true);
|
||||
await pushPrefs(["browser.aboutwelcome.screens", TEST_PROTON_JSON]);
|
||||
|
||||
AttributionCode._clearCache();
|
||||
const data = await AttributionCode.getAttrDataAsync();
|
||||
|
||||
Assert.equal(
|
||||
data.campaign,
|
||||
"set_default_browser",
|
||||
"Attribution campaign should be set"
|
||||
);
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
|
||||
await TestUtils.waitForCondition(() => handleActionStub.called);
|
||||
|
||||
Assert.equal(
|
||||
handleActionStub.firstCall.args[0].type,
|
||||
"SET_DEFAULT_BROWSER",
|
||||
"Set default special message action is called"
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
Services.prefs.getBoolPref(DID_HANDLE_CAMAPAIGN_ACTION_PREF, false),
|
||||
true,
|
||||
"Set default campaign action handled pref is set to true"
|
||||
);
|
||||
|
||||
handleActionStub.reset();
|
||||
// Open a new about:welcome tab to ensure the action does not run again
|
||||
let tab2 = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
|
||||
sinon.assert.notCalled(handleActionStub);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
BrowserTestUtils.removeTab(tab2);
|
||||
await ASRouter.forceAttribution("");
|
||||
Services.prefs.clearUserPref(DID_HANDLE_CAMAPAIGN_ACTION_PREF);
|
||||
Services.prefs.clearUserPref("browser.aboutwelcome.screens");
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
@ -86,7 +86,7 @@ add_task(async function test_aboutwelcome_easy_setup_screen_impression() {
|
||||
.stub(AWScreenUtils, "evaluateScreenTargeting")
|
||||
.resolves(false)
|
||||
.withArgs(
|
||||
"doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser"
|
||||
"doesAppNeedPin && (unhandledCampaignAction != 'SET_DEFAULT_BROWSER') && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser"
|
||||
)
|
||||
.resolves(true)
|
||||
.withArgs("isDeviceMigration")
|
||||
|
@ -137,4 +137,19 @@ describe("AWScreenUtils", () => {
|
||||
assert.equal(addScreenImpressionStub.firstCall.args[0].id, testScreen.id);
|
||||
});
|
||||
});
|
||||
describe("getUnhandledCampaignAction", () => {
|
||||
it("Should call evaluateExpression", () => {
|
||||
const evaluateExpressionStub = sandbox.stub(
|
||||
ASRouter,
|
||||
"evaluateExpression"
|
||||
);
|
||||
AWScreenUtils.getUnhandledCampaignAction();
|
||||
|
||||
assert.calledOnce(evaluateExpressionStub);
|
||||
assert.equal(
|
||||
evaluateExpressionStub.firstCall.args[0].expression,
|
||||
"unhandledCampaignAction"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,13 @@ import { shallow, mount } from "enzyme";
|
||||
import { AboutWelcomeDefaults } from "modules/AboutWelcomeDefaults.sys.mjs";
|
||||
import { AboutWelcomeUtils } from "content-src/lib/aboutwelcome-utils.mjs";
|
||||
|
||||
const spinEventLoop = async () => {
|
||||
// Spin the event loop to allow the useEffect hooks to execute,
|
||||
// any promises to resolve, and re-rendering to happen after the
|
||||
// promises have updated the state/props
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
};
|
||||
|
||||
describe("MultiStageAboutWelcome module", () => {
|
||||
let globals;
|
||||
let sandbox;
|
||||
@ -30,6 +37,7 @@ describe("MultiStageAboutWelcome module", () => {
|
||||
AWEvaluateScreenTargeting: () => {},
|
||||
AWGetSelectedTheme: () => Promise.resolve("automatic"),
|
||||
AWGetInstalledAddons: () => Promise.resolve(["test-addon-id"]),
|
||||
AWGetUnhandledCampaignAction: () => Promise.resolve(false),
|
||||
AWSendEventTelemetry: () => {},
|
||||
AWSendToParent: () => {},
|
||||
AWWaitForMigrationClose: () => Promise.resolve(),
|
||||
@ -53,10 +61,7 @@ describe("MultiStageAboutWelcome module", () => {
|
||||
|
||||
it("should pass activeTheme and initialTheme props to WelcomeScreen", async () => {
|
||||
let wrapper = mount(<MultiStageAboutWelcome {...DEFAULT_PROPS} />);
|
||||
// Spin the event loop to allow the useEffect hooks to execute,
|
||||
// any promises to resolve, and re-rendering to happen after the
|
||||
// promises have updated the state/props
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await spinEventLoop();
|
||||
// sync up enzyme's representation with the real DOM
|
||||
wrapper.update();
|
||||
|
||||
@ -70,10 +75,7 @@ describe("MultiStageAboutWelcome module", () => {
|
||||
|
||||
it("should fetch a list of installed Addons", async () => {
|
||||
let wrapper = mount(<MultiStageAboutWelcome {...DEFAULT_PROPS} />);
|
||||
// Spin the event loop to allow the useEffect hooks to execute,
|
||||
// any promises to resolve, and re-rendering to happen after the
|
||||
// promises have updated the state/props
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await spinEventLoop();
|
||||
// sync up enzyme's representation with the real DOM
|
||||
wrapper.update();
|
||||
|
||||
@ -1132,6 +1134,103 @@ describe("MultiStageAboutWelcome module", () => {
|
||||
AboutWelcomeUtils.handleUserAction.resetHistory();
|
||||
}
|
||||
});
|
||||
it("Should handle a campaign action when applicable", async () => {
|
||||
let actionSpy = sandbox.spy(AboutWelcomeUtils, "handleCampaignAction");
|
||||
let telemetrySpy = sandbox.spy(
|
||||
AboutWelcomeUtils,
|
||||
"sendActionTelemetry"
|
||||
);
|
||||
|
||||
globals.set("AWGetUnhandledCampaignAction", () =>
|
||||
Promise.resolve("SET_DEFAULT_BROWSER")
|
||||
);
|
||||
// Return true when "HANDLE_CAMPAIGN_ACTION" is sent to parent
|
||||
globals.set("AWSendToParent", () => Promise.resolve(true));
|
||||
const screens = [
|
||||
{
|
||||
content: {
|
||||
title: "test title",
|
||||
},
|
||||
},
|
||||
];
|
||||
const TEST_PROPS = {
|
||||
defaultScreens: screens,
|
||||
message_id: "DEFAULT_ABOUTWELCOME",
|
||||
startScreen: 0,
|
||||
};
|
||||
let wrapper = mount(<MultiStageAboutWelcome {...TEST_PROPS} />);
|
||||
await spinEventLoop();
|
||||
wrapper.update();
|
||||
assert.calledOnce(actionSpy);
|
||||
// If campaign is handled, we should send telemetry
|
||||
assert.calledOnce(telemetrySpy);
|
||||
assert.equal(telemetrySpy.firstCall.args[1], "CAMPAIGN_ACTION");
|
||||
globals.restore();
|
||||
});
|
||||
it("Should not handle a campaign action when the action has already been handled", async () => {
|
||||
let actionSpy = sandbox.spy(AboutWelcomeUtils, "handleCampaignAction");
|
||||
let telemetrySpy = sandbox.spy(
|
||||
AboutWelcomeUtils,
|
||||
"sendActionTelemetry"
|
||||
);
|
||||
|
||||
globals.set("AWGetUnhandledCampaignAction", () =>
|
||||
Promise.resolve(false)
|
||||
);
|
||||
const screens = [
|
||||
{
|
||||
content: {
|
||||
title: "test title",
|
||||
},
|
||||
},
|
||||
];
|
||||
const TEST_PROPS = {
|
||||
defaultScreens: screens,
|
||||
message_id: "DEFAULT_ABOUTWELCOME",
|
||||
startScreen: 0,
|
||||
};
|
||||
let wrapper = mount(<MultiStageAboutWelcome {...TEST_PROPS} />);
|
||||
await spinEventLoop();
|
||||
wrapper.update();
|
||||
assert.notCalled(actionSpy);
|
||||
assert.notCalled(telemetrySpy);
|
||||
globals.restore();
|
||||
});
|
||||
it("Should not send telemetrty when campaign action handling fails", async () => {
|
||||
let actionSpy = sandbox.spy(AboutWelcomeUtils, "handleCampaignAction");
|
||||
let telemetrySpy = sandbox.spy(
|
||||
AboutWelcomeUtils,
|
||||
"sendActionTelemetry"
|
||||
);
|
||||
|
||||
globals.set("AWGetUnhandledCampaignAction", () =>
|
||||
Promise.resolve("SET_DEFAULT_BROWSER")
|
||||
);
|
||||
|
||||
// Return undefined when "HANDLE_CAMPAIGN_ACTION" is sent to parent as
|
||||
// though "AWPage:HANDLE_CAMPAIGN_ACTION" case did not return true due
|
||||
// to failure executing action or the campaign handled pref being true
|
||||
globals.set("AWSendToParent", () => Promise.resolve(undefined));
|
||||
const screens = [
|
||||
{
|
||||
content: {
|
||||
title: "test title",
|
||||
},
|
||||
},
|
||||
];
|
||||
const TEST_PROPS = {
|
||||
defaultScreens: screens,
|
||||
message_id: "DEFAULT_ABOUTWELCOME",
|
||||
startScreen: 0,
|
||||
};
|
||||
let wrapper = mount(<MultiStageAboutWelcome {...TEST_PROPS} />);
|
||||
await spinEventLoop();
|
||||
wrapper.update();
|
||||
assert.calledOnce(actionSpy);
|
||||
// If campaign handling fails, we should not send telemetry
|
||||
assert.notCalled(telemetrySpy);
|
||||
globals.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#handleUserAction", () => {
|
||||
|
@ -49,6 +49,7 @@ Please note that some targeting attributes require stricter controls on the tele
|
||||
* [isMajorUpgrade](#ismajorupgrade)
|
||||
* [isMSIX](#ismsix)
|
||||
* [isRTAMO](#isrtamo)
|
||||
* [unhandledCampaignAction](#unhandledCampaignAction)
|
||||
* [launchOnLoginEnabled](#launchonloginenabled)
|
||||
* [locale](#locale)
|
||||
* [localeLanguageCode](#localelanguagecode)
|
||||
@ -1041,6 +1042,10 @@ A boolean. `true` when both the current install and current profile support crea
|
||||
|
||||
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.
|
||||
|
||||
### `unhandledCampaignAction`
|
||||
|
||||
A string. A special message action to be executed on first-run. For example, `"SET_DEFAULT_BROWSER"` when the user selected to set as default via the [install marketing page](https://www.mozilla.org/firefox/new/) and set default has not yet been automatically triggered, `null` otherwise.
|
||||
|
||||
### `isMSIX`
|
||||
|
||||
A boolean. `true` when hasPackageId is `true` on Windows, `false` otherwise.
|
||||
|
@ -176,6 +176,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
"toolkit.profiles.storeID",
|
||||
null
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"didHandleCampaignAction",
|
||||
"trailhead.firstrun.didHandleCampaignAction",
|
||||
false
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetters(lazy, {
|
||||
AUS: ["@mozilla.org/updates/update-service;1", "nsIApplicationUpdateService"],
|
||||
@ -258,6 +264,39 @@ function CacheListAttachedOAuthClients() {
|
||||
};
|
||||
}
|
||||
|
||||
function CacheUnhandledCampaignAction() {
|
||||
return {
|
||||
_lastUpdated: 0,
|
||||
_value: null,
|
||||
expire() {
|
||||
this._lastUpdated = 0;
|
||||
this._value = null;
|
||||
},
|
||||
get() {
|
||||
const now = Date.now();
|
||||
// Don't get cached value until the action has been handled to ensure
|
||||
// proper screen targeting in about:welcome
|
||||
if (
|
||||
now - this._lastUpdated >= FRECENT_SITES_UPDATE_INTERVAL ||
|
||||
!lazy.didHandleCampaignAction
|
||||
) {
|
||||
this._value = null;
|
||||
if (!lazy.didHandleCampaignAction) {
|
||||
const attributionData =
|
||||
lazy.AttributionCode.getCachedAttributionData();
|
||||
const ALLOWED_CAMPAIGN_ACTIONS = ["SET_DEFAULT_BROWSER"];
|
||||
const campaign = attributionData?.campaign?.toUpperCase();
|
||||
if (campaign && ALLOWED_CAMPAIGN_ACTIONS.includes(campaign)) {
|
||||
this._value = campaign;
|
||||
}
|
||||
}
|
||||
this._lastUpdated = now;
|
||||
}
|
||||
return this._value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function CheckBrowserNeedsUpdate(
|
||||
updateInterval = FRECENT_SITES_UPDATE_INTERVAL
|
||||
) {
|
||||
@ -326,6 +365,7 @@ export const QueryCache = {
|
||||
RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"),
|
||||
ListAttachedOAuthClients: new CacheListAttachedOAuthClients(),
|
||||
UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"),
|
||||
UnhandledCampaignAction: new CacheUnhandledCampaignAction(),
|
||||
},
|
||||
getters: {
|
||||
doesAppNeedPin: new CachedTargetingGetter(
|
||||
@ -1066,6 +1106,17 @@ const TargetingGetters = {
|
||||
return attributionData?.campaign === "migration";
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the user opted into a special message action represented by an
|
||||
* installer attribution campaign and this choice still needs to be honored.
|
||||
* @return {string} A special message action to be executed on first-run. For
|
||||
* example, `"SET_DEFAULT_BROWSER"` when the user selected to set as default
|
||||
* via the install marketing page and set default has not yet been
|
||||
* automatically triggered, 'null' otherwise.
|
||||
*/
|
||||
get unhandledCampaignAction() {
|
||||
return QueryCache.queries.UnhandledCampaignAction.get();
|
||||
},
|
||||
/**
|
||||
* The values of the height and width available to the browser to display
|
||||
* web content. The available height and width are each calculated taking
|
||||
|
@ -1859,3 +1859,79 @@ add_task(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function check_unhandledCampaignAction() {
|
||||
is(
|
||||
typeof ASRouterTargeting.Environment.unhandledCampaignAction,
|
||||
"object",
|
||||
"Should return an object" // is null unless an unhandled action is present
|
||||
);
|
||||
|
||||
const DID_HANDLE_CAMAPAIGN_ACTION_PREF =
|
||||
"trailhead.firstrun.didHandleCampaignAction";
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
title: "unsupported open_url campaign action",
|
||||
attributionData: {
|
||||
campaign: "open_url",
|
||||
},
|
||||
expected: null,
|
||||
after: () => {
|
||||
QueryCache.queries.UnhandledCampaignAction.expire();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "supported and unhandled set default browser campaign action",
|
||||
attributionData: {
|
||||
campaign: "set_default_browser",
|
||||
},
|
||||
expected: "SET_DEFAULT_BROWSER",
|
||||
after: () => {
|
||||
QueryCache.queries.UnhandledCampaignAction.expire();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "supported and handled set default browser campaign action",
|
||||
attributionData: {
|
||||
campaign: "set_default_browser",
|
||||
},
|
||||
expected: null,
|
||||
before: async () => {
|
||||
await pushPrefs([DID_HANDLE_CAMAPAIGN_ACTION_PREF, true]);
|
||||
},
|
||||
after: () => {
|
||||
Services.prefs.clearUserPref(DID_HANDLE_CAMAPAIGN_ACTION_PREF);
|
||||
QueryCache.queries.UnhandledCampaignAction.expire();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
registerCleanupFunction(async () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
const stub = sandbox.stub(AttributionCode, "getCachedAttributionData");
|
||||
|
||||
for (const {
|
||||
title,
|
||||
attributionData,
|
||||
expected,
|
||||
before,
|
||||
after,
|
||||
} of TEST_CASES) {
|
||||
if (before) {
|
||||
await before();
|
||||
}
|
||||
stub.returns(attributionData);
|
||||
is(
|
||||
ASRouterTargeting.Environment.unhandledCampaignAction,
|
||||
expected,
|
||||
`${title} - Expected unhandledCampaignAction to have the expected value`
|
||||
);
|
||||
if (after) {
|
||||
after();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user