Bug 1303510 - Part 1: Implement doorhanger helper and parameter for first time use doorhanger. r?MattN draft
authorsteveck-chung <schung@mozilla.com>
Fri, 19 May 2017 17:48:16 +0800
changeset 591574 8c194a54225f909855a2f19c8323182e35820b4f
parent 591406 f4262773c4331d4ae139be536ce278ea9aad3436
child 591575 22831e364b5583fbd056a75a2ca46655ad713b7e
child 591675 2377006875740f09fe5c16af2bf297284b7d2b48
push id63092
push userbmo:schung@mozilla.com
push dateFri, 09 Jun 2017 07:23:42 +0000
reviewersMattN
bugs1303510
milestone55.0a1
Bug 1303510 - Part 1: Implement doorhanger helper and parameter for first time use doorhanger. r?MattN MozReview-Commit-ID: EQPXlyYzE6z
browser/app/profile/firefox.js
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillDoorhanger.jsm
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/content/icon-address-save.svg
browser/extensions/formautofill/locale/en-US/formautofill.properties
browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1658,16 +1658,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],