Bug 1303510 - Part 1: Implement doorhanger helper and parameter for first time use doorhanger. r=lchang
MozReview-Commit-ID: 3LS5wEh6JlU
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1664,16 +1664,17 @@ pref("browser.crashReports.unsubmittedCh
// Preferences for the form autofill system extension
#ifdef NIGHTLY_BUILD
pref("extensions.formautofill.experimental", true);
#else
pref("extensions.formautofill.experimental", false);
#endif
pref("extensions.formautofill.addresses.enabled", true);
+pref("extensions.formautofill.firstTimeUse", true);
pref("extensions.formautofill.heuristics.enabled", true);
pref("extensions.formautofill.loglevel", "Warn");
// Whether or not to restore a session with lazy-browser tabs.
pref("browser.sessionstore.restore_tabs_lazily", true);
// Enable safebrowsing v4 tables (suffixed by "-proto") update.
#ifdef NIGHTLY_BUILD
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -319,19 +319,21 @@ var FormAutofillContent = {
this.savedFieldNames =
Services.cpmm.initialProcessData.autofillSavedFieldNames;
},
/**
* Send the profile to parent for doorhanger and storage saving/updating.
*
* @param {Object} profile Submitted form's address/creditcard guid and record.
+ * @param {Object} domWin Current content window.
*/
- _onFormSubmit(profile) {
- Services.cpmm.sendAsyncMessage("FormAutofill:OnFormSubmit", profile);
+ _onFormSubmit(profile, domWin) {
+ let mm = this._messageManagerFromWindow(domWin);
+ mm.sendAsyncMessage("FormAutofill:OnFormSubmit", profile);
},
/**
* Handle earlyformsubmit event and early return when:
* 1. In private browsing mode.
* 2. Could not map any autofill handler by form element.
* 3. Number of filled fields is less than autofill threshold
*
@@ -360,17 +362,17 @@ var FormAutofillContent = {
}
this._onFormSubmit({
address: {
guid: handler.filledProfileGUID,
record: pendingAddress,
},
// creditCard: {}
- });
+ }, domWin);
return true;
},
receiveMessage({name, data}) {
switch (name) {
case "FormAutofill:enabledStatus": {
if (data) {
@@ -502,12 +504,20 @@ var FormAutofillContent = {
let selectedIndex = ProfileAutocomplete._getSelectedIndex(doc.ownerGlobal);
if (selectedIndex === -1) {
ProfileAutocomplete._clearProfilePreview();
} else {
ProfileAutocomplete._previewSelectedProfile(selectedIndex);
}
},
+
+ _messageManagerFromWindow(win) {
+ return win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ },
};
FormAutofillContent.init();
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/FormAutofillDoorhanger.jsm
@@ -0,0 +1,173 @@
+/* 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/. */
+
+/*
+ * Implements doorhanger singleton that wraps up the PopupNotifications and handles
+ * the doorhager UI for formautofill related features.
+ */
+
+/* exported FormAutofillDoorhanger */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FormAutofillDoorhanger"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
+this.log = null;
+FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
+
+const BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties";
+const GetStringFromName = Services.strings.createBundle(BUNDLE_URI).GetStringFromName;
+
+const CONTENT = {
+ firstTimeUse: {
+ notificationId: "autofill-address",
+ message: GetStringFromName("saveAddressMessage"),
+ anchor: {
+ id: "autofill-address-notification-icon",
+ URL: "chrome://formautofill/content/icon-address-save.svg",
+ tooltiptext: GetStringFromName("openAutofillMessagePanel"),
+ },
+ options: {
+ persistWhileVisible: true,
+ popupIconURL: "chrome://formautofill/content/icon-address-save.svg",
+ },
+ },
+};
+
+let FormAutofillDoorhanger = {
+ /**
+ * Generate the main action and secondary actions from content parameters and
+ * promise resolve.
+ *
+ * @private
+ * @param {Object} mainActionParams
+ * Parameters for main action.
+ * @param {Array<Object>} secondaryActionParams
+ * Array of the parameters for secondary actions.
+ * @param {Function} resolve Should be called in action callback.
+ * @returns {Array<Object>}
+ Return the mainAction and secondary actions in an array for showing doorhanger
+ */
+ _createActions(mainActionParams, secondaryActionParams, resolve) {
+ if (!mainActionParams) {
+ return [null, null];
+ }
+
+ let {label, accessKey, callbackState} = mainActionParams;
+ let callback = resolve.bind(null, callbackState);
+ let mainAction = {label, accessKey, callback};
+
+ if (!secondaryActionParams) {
+ return [mainAction, null];
+ }
+
+ let secondaryActions = [];
+ for (let params of secondaryActionParams) {
+ let cb = resolve.bind(null, params.callbackState);
+ secondaryActions.push({
+ label: params.label,
+ accessKey: params.accessKey,
+ callback: cb,
+ });
+ }
+
+ return [mainAction, secondaryActions];
+ },
+ /**
+ * Append the link label element to the popupnotificationcontent.
+ * @param {XULElement} browser
+ * Target browser element for showing doorhanger.
+ * @param {string} id
+ * The ID of the doorhanger.
+ */
+ _appendPrivacyPanelLink(browser, id) {
+ let notificationId = id + "-notification";
+ let chromeDoc = browser.ownerDocument;
+ let notification = chromeDoc.getElementById(notificationId);
+
+ if (!notification.querySelector("popupnotificationcontent")) {
+ let notificationcontent = chromeDoc.createElement("popupnotificationcontent");
+ let privacyLinkElement = chromeDoc.createElement("label");
+ privacyLinkElement.className = "text-link";
+ privacyLinkElement.setAttribute("useoriginprincipal", true);
+ privacyLinkElement.setAttribute("href", "about:preferences#privacy");
+ privacyLinkElement.setAttribute("value", GetStringFromName("viewAutofillOptions"));
+ notificationcontent.appendChild(privacyLinkElement);
+ notification.append(notificationcontent);
+ }
+ },
+ /**
+ * Create an image element for notification anchor if it doesn't already exist.
+ * @param {XULElement} browser
+ * Target browser element for showing doorhanger.
+ * @param {Object} anchor
+ * Anchor options for setting the anchor element.
+ * @param {string} anchor.id
+ * ID of the anchor element.
+ * @param {string} anchor.URL
+ * Path of the icon asset.
+ * @param {string} anchor.tooltiptext
+ * Tooltip string for the anchor.
+ */
+ _setAnchor(browser, anchor) {
+ let chromeDoc = browser.ownerDocument;
+ let {id, URL, tooltiptext} = anchor;
+ let anchorEt = chromeDoc.getElementById(id);
+ if (!anchorEt) {
+ let notificationPopupBox =
+ chromeDoc.getElementById("notification-popup-box");
+ // Icon shown on URL bar
+ let anchorElement = chromeDoc.createElement("image");
+ anchorElement.id = id;
+ anchorElement.setAttribute("src", URL);
+ anchorElement.classList.add("notification-anchor-icon");
+ anchorElement.setAttribute("role", "button");
+ anchorElement.setAttribute("tooltiptext", tooltiptext);
+ anchorElement.style.setProperty("-moz-context-properties", "fill");
+ anchorElement.style.fill = "currentcolor";
+ notificationPopupBox.appendChild(anchorElement);
+ }
+ },
+ /**
+ * Show different types of doorhanger by leveraging PopupNotifications.
+ * @param {XULElement} browser
+ * Target browser element for showing doorhanger.
+ * @param {string} type
+ * The type of the doorhanger. There will have first time use/update/credit card.
+ * @returns {Promise}
+ Resolved with action type when action callback is triggered.
+ */
+ async show(browser, type) {
+ log.debug("show doorhanger with type:", type);
+ return new Promise((resolve) => {
+ let content = CONTENT[type];
+ let chromeWin = browser.ownerGlobal;
+ content.options.eventCallback = (topic) => {
+ log.debug("eventCallback:", topic);
+
+ switch (topic) {
+ // We can only append label element when notification box is shown
+ case "shown":
+ this._appendPrivacyPanelLink(browser, content.notificationId);
+ break;
+ }
+ };
+ this._setAnchor(browser, content.anchor);
+ chromeWin.PopupNotifications.show(
+ browser,
+ content.notificationId,
+ content.message,
+ content.anchor.id,
+ ...this._createActions(content.mainAction, content.secondaryActions, resolve),
+ content.options,
+ );
+ });
+ },
+};
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -30,22 +30,23 @@
"use strict";
this.EXPORTED_SYMBOLS = ["FormAutofillParent"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillPreferences",
"resource://formautofill/FormAutofillPreferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillDoorhanger",
+ "resource://formautofill/FormAutofillDoorhanger.jsm");
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
const ENABLED_PREF = "extensions.formautofill.addresses.enabled";
function FormAutofillParent() {
// Lazily load the storage JSM to avoid disk I/O until absolutely needed.
@@ -75,17 +76,17 @@ FormAutofillParent.prototype = {
* Initializes ProfileStorage and registers the message handler.
*/
async init() {
Services.obs.addObserver(this, "advanced-pane-loaded");
Services.ppmm.addMessageListener("FormAutofill:InitStorage", this);
Services.ppmm.addMessageListener("FormAutofill:GetAddresses", this);
Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this);
Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this);
- Services.ppmm.addMessageListener("FormAutofill:OnFormSubmit", this);
+ Services.mm.addMessageListener("FormAutofill:OnFormSubmit", this);
// Observing the pref and storage changes
Services.prefs.addObserver(ENABLED_PREF, this);
Services.obs.addObserver(this, "formautofill-storage-changed");
},
observe(subject, topic, data) {
log.debug("observe:", topic, "with data:", data);
@@ -271,13 +272,21 @@ FormAutofillParent.prototype = {
if (address.guid) {
if (!this.profileStorage.addresses.mergeIfPossible(address.guid, address.record)) {
// TODO: Show update doorhanger(bug 1303513) and set probe(bug 990200)
return;
}
this.profileStorage.addresses.notifyUsed(address.guid);
} else {
- // TODO: Add first time use probe(bug 990199) and doorhanger(bug 1303510)
- // profileStorage.addresses.add(address.record);
+ if (!Services.prefs.getBoolPref("extensions.formautofill.firstTimeUse")) {
+ if (!this.profileStorage.addresses.mergeToStorage(address.record)) {
+ this.profileStorage.addresses.add(address.record);
+ }
+ return;
+ }
+
+ this.profileStorage.addresses.add(address.record);
+ Services.prefs.setBoolPref("extensions.formautofill.firstTimeUse", false);
+ FormAutofillDoorhanger.show(target, "firstTimeUse");
}
},
};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-address-save.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+ <path fill="#999899" d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm21.1-1.9h-2.5V20c0-.4-.3-.8-.8-.8h-3.1c-.4 0-.8.3-.8.8v4.6h-2.5c-.6 0-.8.4-.3.8l4.3 4.2c.2.2.5.3.8.3s.6-.1.8-.3l4.3-4.2c.6-.4.4-.7-.2-.7zm-11.3-5.6H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2z"/>
+</svg>
--- a/browser/extensions/formautofill/locale/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locale/en-US/formautofill.properties
@@ -1,7 +1,10 @@
# 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/.
preferenceGroupTitle = Form Autofill
enableProfileAutofill = Enable Profile Autofill
savedProfiles = Saved Profiles…
+saveAddressMessage = Firefox now saves your form data to help you fill out forms faster!
+viewAutofillOptions = View Form Autofill options…
+openAutofillMessagePanel = Open Form Autofill message panel
--- a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
+++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
@@ -103,17 +103,17 @@ TESTCASES.forEach(testcase => {
add_task(async function check_profile_saving_is_called_correctly() {
do_print("Starting testcase: " + testcase.description);
let form = MOCK_DOC.getElementById("form1");
for (let key in testcase.formValue) {
let input = MOCK_DOC.getElementById(key);
input.value = testcase.formValue[key];
}
- sinon.spy(FormAutofillContent, "_onFormSubmit");
+ sinon.stub(FormAutofillContent, "_onFormSubmit");
FormAutofillContent.identifyAutofillFields(MOCK_DOC);
FormAutofillContent.notify(form);
do_check_eq(FormAutofillContent._onFormSubmit.called,
testcase.expectedResult.formSubmission);
if (FormAutofillContent._onFormSubmit.called) {
Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0],