Bug 1384045 - Show different contents in sync tour of onboarding for signed-in user.r=flod,mossop
authorRex Lee <rexboy@mozilla.com>
Fri, 28 Jul 2017 16:54:55 +0800
changeset 420698 c8e26d0c9d9ac687d8568fd345c623698ecdd4c4
parent 420697 d10315bb91381dac99cc8ba609d875a04e6543cd
child 420699 f5922a7ed25a9aa0683f57bb43e2a53709c385ee
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, mossop
bugs1384045
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1384045 - Show different contents in sync tour of onboarding for signed-in user.r=flod,mossop MozReview-Commit-ID: BH2f4ujdHbG
browser/extensions/onboarding/bootstrap.js
browser/extensions/onboarding/content/onboarding.css
browser/extensions/onboarding/content/onboarding.js
browser/extensions/onboarding/locales/en-US/onboarding.properties
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -55,76 +55,95 @@ function setPrefs(prefs) {
   prefs.forEach(pref => {
     if (PREF_WHITELIST.includes(pref.name)) {
       Preferences.set(pref.name, pref.value);
     }
   });
 }
 
 /**
+ * syncTourChecker listens to and maintains the login status inside, and can be
+ * queried at any time once initialized.
+ */
+let syncTourChecker = {
+  _registered: false,
+  _loggedIn: false,
+
+  isLoggedIn() {
+    return this._loggedIn;
+  },
+
+  observe(subject, topic) {
+    switch (topic) {
+      case "fxaccounts:onlogin":
+        this.setComplete();
+        break;
+      case "fxaccounts:onlogout":
+        this._loggedIn = false;
+        break;
+    }
+  },
+
+  init() {
+    // Check if we've already logged in at startup.
+    fxAccounts.getSignedInUser().then(user => {
+      if (user) {
+        this.setComplete();
+      }
+      // Observe for login action if we haven't logged in yet.
+      this.register();
+    });
+  },
+
+  register() {
+    if (this._registered) {
+      return;
+    }
+    Services.obs.addObserver(this, "fxaccounts:onlogin");
+    Services.obs.addObserver(this, "fxaccounts:onlogout");
+    this._registered = true;
+  },
+
+  setComplete() {
+    this._loggedIn = true;
+    Services.prefs.setBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", true);
+  },
+
+  unregister() {
+    if (!this._registered) {
+      return;
+    }
+    Services.obs.removeObserver(this, "fxaccounts:onlogin");
+    Services.obs.removeObserver(this, "fxaccounts:onlogout");
+    this._registered = false;
+  },
+
+  uninit() {
+    this.unregister();
+  },
+}
+
+/**
  * Listen and process events from content.
  */
 function initContentMessageListener() {
   Services.mm.addMessageListener("Onboarding:OnContentMessage", msg => {
     switch (msg.data.action) {
       case "set-prefs":
         setPrefs(msg.data.params);
         break;
+      case "get-login-status":
+        msg.target.messageManager.sendAsyncMessage("Onboarding:ResponseLoginStatus", {
+          isLoggedIn: syncTourChecker.isLoggedIn()
+        });
+        break;
     }
   });
 }
 
-let syncTourChecker = {
-  registered: false,
-
-  observe() {
-    this.setComplete();
-  },
-
-  init() {
-    if (Services.prefs.getBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", false)) {
-      return;
-    }
-    // Check if we've already logged in at startup.
-    fxAccounts.getSignedInUser().then(user => {
-      if (user) {
-        this.setComplete();
-        return;
-      }
-      // Observe for login action if we haven't logged in yet.
-      this.register();
-    });
-  },
-
-  register() {
-    if (this.registered) {
-      return;
-    }
-    Services.obs.addObserver(this, "fxaccounts:onverified");
-    this.registered = true;
-  },
-
-  setComplete() {
-    Services.prefs.setBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", true);
-    this.unregister();
-  },
-
-  unregister() {
-    if (!this.registered) {
-      return;
-    }
-    Services.obs.removeObserver(this, "fxaccounts:onverified");
-    this.registered = false;
-  },
-
-  uninit() {
-    this.unregister();
-  },
-}
-
 /**
  * onBrowserReady - Continues startup of the add-on after browser is ready.
  */
 function onBrowserReady() {
   waitingForBrowserReady = false;
 
   OnboardingTourType.check();
   Services.mm.loadFrameScript("resource://onboarding/onboarding.js", true);
--- a/browser/extensions/onboarding/content/onboarding.css
+++ b/browser/extensions/onboarding/content/onboarding.css
@@ -56,17 +56,19 @@
   border-radius: 22px;
   padding: 5px 8px;
   width: 110px;
   margin-inline-start: 3px;
   margin-top: -5px;
 }
 
 #onboarding-overlay-dialog,
-.onboarding-hidden {
+.onboarding-hidden,
+#onboarding-tour-sync-page[data-login-state=logged-in] .show-on-logged-out,
+#onboarding-tour-sync-page[data-login-state=logged-out] .show-on-logged-in {
   display: none;
 }
 
 .onboarding-close-btn {
    position: absolute;
    top: 15px;
    offset-inline-end: 15px;
    cursor: pointer;
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -189,42 +189,57 @@ var onboardingTourset = {
     getNotificationStrings(bundle) {
       return {
         title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-sync.title"),
         message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-sync.message"),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
     },
     getPage(win, bundle) {
+      const STATE_LOGOUT = "logged-out";
+      const STATE_LOGIN = "logged-in";
       let div = win.document.createElement("div");
       div.classList.add("onboarding-no-button");
+      div.dataset.loginState = STATE_LOGOUT;
       // The email validation pattern used in the form comes from IETF rfc5321,
       // which is identical to server-side checker of Firefox Account. See
       // discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1378770#c6
       // for detail.
       let emailRegex = "^[\\w.!#$%&’*+\\/=?^`{|}~-]{1,64}@[a-z\\d](?:[a-z\\d-]{0,253}[a-z\\d])?(?:\\.[a-z\\d](?:[a-z\\d-]{0,253}[a-z\\d])?)+$";
       div.innerHTML = `
         <section class="onboarding-tour-description">
-          <h1 data-l10n-id="onboarding.tour-sync.title2"></h1>
-          <p data-l10n-id="onboarding.tour-sync.description2"></p>
+          <h1 data-l10n-id="onboarding.tour-sync.title2" class="show-on-logged-out"></h1>
+          <p data-l10n-id="onboarding.tour-sync.description2" class="show-on-logged-out"></p>
+          <h1 data-l10n-id="onboarding.tour-sync.logged-in.title" class="show-on-logged-in"></h1>
+          <p data-l10n-id="onboarding.tour-sync.logged-in.description" class="show-on-logged-in"></p>
         </section>
         <section class="onboarding-tour-content">
-          <form>
+          <form class="show-on-logged-out">
             <h3 data-l10n-id="onboarding.tour-sync.form.title"></h3>
             <p data-l10n-id="onboarding.tour-sync.form.description"></p>
             <input id="onboarding-tour-sync-email-input" type="email" required="true"></input><br />
             <button id="onboarding-tour-sync-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-sync.button"></button>
           </form>
           <img src="resource://onboarding/img/figure_sync.svg" role="presentation"/>
         </section>
       `;
       let emailInput = div.querySelector("#onboarding-tour-sync-email-input");
       emailInput.placeholder =
         bundle.GetStringFromName("onboarding.tour-sync.email-input.placeholder");
       emailInput.pattern = emailRegex;
+
+      div.addEventListener("beforeshow", () => {
+        function loginStatusListener(msg) {
+          removeMessageListener("Onboarding:ResponseLoginStatus", loginStatusListener);
+          div.dataset.loginState = msg.data.isLoggedIn ? STATE_LOGIN : STATE_LOGOUT;
+        }
+        sendMessageToChrome("get-login-status");
+        addMessageListener("Onboarding:ResponseLoginStatus", loginStatusListener);
+      });
+
       return div;
     },
   },
   "library": {
     id: "onboarding-tour-library",
     tourNameId: "onboarding.tour-library",
     getNotificationStrings(bundle) {
       return {
@@ -301,16 +316,25 @@ var onboardingTourset = {
         </section>
       `;
       return div;
     },
   },
 };
 
 /**
+ * @param {String} action the action to ask the chrome to do
+ * @param {Array} params the parameters for the action
+ */
+function sendMessageToChrome(action, params) {
+  sendAsyncMessage("Onboarding:OnContentMessage", {
+    action, params
+  });
+}
+/**
  * The script won't be initialized if we turned off onboarding by
  * setting "browser.onboarding.enabled" to false.
  */
 class Onboarding {
   constructor(contentWindow) {
     this.init(contentWindow);
   }
 
@@ -429,26 +453,16 @@ class Onboarding {
     if (this._prefsObserved) {
       for (let [name, callback] of this._prefsObserved) {
         Preferences.ignore(name, callback);
       }
       this._prefsObserved = null;
     }
   }
 
-  /**
-   * @param {String} action the action to ask the chrome to do
-   * @param {Array} params the parameters for the action
-   */
-  sendMessageToChrome(action, params) {
-    sendAsyncMessage("Onboarding:OnContentMessage", {
-      action, params
-    });
-  }
-
   handleEvent(evt) {
     if (evt.type === "resize") {
       this._window.cancelIdleCallback(this._resizeTimerId);
       this._resizeTimerId =
         this._window.requestIdleCallback(() => this._resizeUI());
 
       return;
     }
@@ -520,17 +534,22 @@ class Onboarding {
     if (hiddenCheckbox.checked) {
       this.hide();
     }
   }
 
   gotoPage(tourId) {
     let targetPageId = `${tourId}-page`;
     for (let page of this._tourPages) {
-      page.style.display = page.id != targetPageId ? "none" : "";
+      if (page.id === targetPageId) {
+        page.style.display = "";
+        page.dispatchEvent(new this._window.CustomEvent("beforeshow"));
+      } else {
+        page.style.display = "none";
+      }
     }
     for (let li of this._tourItems) {
       if (li.id == tourId) {
         li.classList.add("onboarding-active");
       } else {
         li.classList.remove("onboarding-active");
       }
     }
@@ -546,17 +565,17 @@ class Onboarding {
       if (!this.isTourCompleted(id)) {
         params.push({
           name: `browser.onboarding.tour.${id}.completed`,
           value: true
         });
       }
     });
     if (params.length > 0) {
-      this.sendMessageToChrome("set-prefs", params);
+      sendMessageToChrome("set-prefs", params);
     }
   }
 
   markTourCompletionState(tourId) {
     // We are doing lazy load so there might be no items.
     if (this._tourItems && this._tourItems.length > 0 && this.isTourCompleted(tourId)) {
       let targetItem = this._tourItems.find(item => item.id == tourId);
       targetItem.classList.add("onboarding-complete");
@@ -574,17 +593,17 @@ class Onboarding {
       // Don't mute when this is set to 0 on purpose.
       return false;
     }
 
     // Reuse the `last-time-of-changing-tour-sec` to save the time that
     // we try to prompt on the 1st session.
     let lastTime = 1000 * Preferences.get("browser.onboarding.notification.last-time-of-changing-tour-sec", 0);
     if (lastTime <= 0) {
-      this.sendMessageToChrome("set-prefs", [{
+      sendMessageToChrome("set-prefs", [{
         name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
         value: Math.floor(Date.now() / 1000)
       }]);
       return true;
     }
     return Date.now() - lastTime <= muteDuration;
   }
 
@@ -614,34 +633,34 @@ class Onboarding {
     params.push({
       name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
       value: 0
     });
     params.push({
       name: "browser.onboarding.notification.prompt-count",
       value: 0
     });
-    this.sendMessageToChrome("set-prefs", params);
+    sendMessageToChrome("set-prefs", params);
   }
 
   _getNotificationQueue() {
     let queue = "";
     if (Preferences.isSet("browser.onboarding.notification.tour-ids-queue")) {
       queue = Preferences.get("browser.onboarding.notification.tour-ids-queue");
     } else {
       // For each tour, it only gets 2 chances to prompt with notification
       // (each chance includes 8 impressions or 5-days max life time)
       // if user never interact with it.
       // Assume there are tour #0 ~ #5. Here would form the queue as
       // "#0,#1,#2,#3,#4,#5,#0,#1,#2,#3,#4,#5".
       // Then we would loop through this queue and remove prompted tour from the queue
       // until the queue is empty.
       let ids = this._tours.map(tour => tour.id).join(",");
       queue = `${ids},${ids}`;
-      this.sendMessageToChrome("set-prefs", [{
+      sendMessageToChrome("set-prefs", [{
         name: "browser.onboarding.notification.tour-ids-queue",
         value: queue
       }]);
     }
     return queue ? queue.split(",") : [];
   }
 
   showNotification() {
@@ -660,17 +679,17 @@ class Onboarding {
       queue.shift();
     }
     // We don't want to prompt completed tour.
     while (queue.length > 0 && this.isTourCompleted(queue[0])) {
       queue.shift();
     }
 
     if (queue.length == 0) {
-      this.sendMessageToChrome("set-prefs", [
+      sendMessageToChrome("set-prefs", [
         {
           name: "browser.onboarding.notification.finished",
           value: true
         },
         {
           name: "browser.onboarding.notification.tour-ids-queue",
           value: ""
         }
@@ -711,17 +730,17 @@ class Onboarding {
       });
     } else {
       let promptCount = Preferences.get(PROMPT_COUNT_PREF, 0);
       params.push({
         name: PROMPT_COUNT_PREF,
         value: promptCount + 1
       });
     }
-    this.sendMessageToChrome("set-prefs", params);
+    sendMessageToChrome("set-prefs", params);
   }
 
   hideNotification() {
     if (this._notificationBar) {
       this._notificationBar.classList.remove("onboarding-opened");
     }
   }
 
@@ -751,17 +770,17 @@ class Onboarding {
     let closeBtn = div.querySelector("#onboarding-notification-close-btn");
     closeBtn.setAttribute("title",
       this._bundle.GetStringFromName("onboarding.notification-close-button-tooltip"));
     return div;
   }
 
   hide() {
     this.setToursCompleted(this._tours.map(tour => tour.id));
-    this.sendMessageToChrome("set-prefs", [
+    sendMessageToChrome("set-prefs", [
       {
         name: "browser.onboarding.hidden",
         value: true
       },
       {
         name: "browser.onboarding.notification.finished",
         value: true
       }
--- a/browser/extensions/onboarding/locales/en-US/onboarding.properties
+++ b/browser/extensions/onboarding/locales/en-US/onboarding.properties
@@ -72,16 +72,19 @@ onboarding.tour-default-browser.is-defau
 # LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.title): This string will be used in the notification title for the default browser tour. %S is brandShortName.
 onboarding.notification.onboarding-tour-default-browser.title=Make %S your go-to browser.
 # LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.message): This string will be used in the notification message for the default browser tour. %1$S is brandShortName
 onboarding.notification.onboarding-tour-default-browser.message=It doesn’t take much to get the most from %1$S. Just set %1$S as your default browser and put control, customization, and protection on autopilot.
 
 onboarding.tour-sync2=Sync
 onboarding.tour-sync.title2=Pick up where you left off.
 onboarding.tour-sync.description2=Sync makes it easy to access bookmarks, passwords, and even open tabs on all your devices. Sync also gives you control of the types of information you want, and don’t want, to share.
+onboarding.tour-sync.logged-in.title=You’re signed in to Sync!
+# LOCALIZATION NOTE(onboarding.tour-sync.logged-in.description): %1$S is brandShortName.
+onboarding.tour-sync.logged-in.description=Sync works when you’re signed in to %1$S on more than one device. Have a mobile device? Install the %1$S app and sign in to get your bookmarks, history, and passwords on the go.
 # LOCALIZATION NOTE(onboarding.tour-sync.form.title): This string is displayed
 # as a title and followed by onboarding.tour-sync.form.description.
 # Your translation should be consistent with the form displayed in
 # about:accounts when signing up to Firefox Account.
 onboarding.tour-sync.form.title=Create a Firefox Account
 # LOCALIZATION NOTE(onboarding.tour-sync.form.description): The description
 # continues after onboarding.tour-sync.form.title to create a complete sentence.
 # If it's not possible for your locale, you can translate this string as