Bug 1375775 - Allow reuse existing tours;r=mossop draft
authorgasolin <gasolin@gmail.com>
Mon, 26 Jun 2017 14:25:55 -0700
changeset 606526 801f0eae5e22a6023cb298bc79b774d32f1c35e9
parent 606416 5d794bf4c4653153e602631f1b8818acd559d8f5
child 606527 227b666431fa0dba121392be0d11d0859e71c639
push id67714
push userbmo:gasolin@mozilla.com
push dateTue, 11 Jul 2017 01:35:10 +0000
reviewersmossop
bugs1375775
milestone56.0a1
Bug 1375775 - Allow reuse existing tours;r=mossop MozReview-Commit-ID: 4KwLBH5oPXY
browser/app/profile/firefox.js
browser/extensions/onboarding/README.md
browser/extensions/onboarding/content/onboarding.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1699,16 +1699,18 @@ pref("browser.suppress_first_window_anim
 pref("browser.onboarding.enabled", true);
 // Mark this as an upgraded profile so we don't offer the initial new user onboarding tour.
 pref("browser.onboarding.tourset-version", 1);
 pref("browser.onboarding.hidden", false);
 // On the Activity-Stream page, the snippet's position overlaps with our notification.
 // So use `browser.onboarding.notification.finished` to let the AS page know
 // if our notification is finished and safe to show their snippet.
 pref("browser.onboarding.notification.finished", false);
+pref("browser.onboarding.newtour", "private,addons,customize,search,default,sync");
+pref("browser.onboarding.updatetour", "");
 
 // Preferences for the Screenshots feature:
 // Temporarily disable Screenshots in Beta & Release, so that we can gradually
 // roll out the feature using SHIELD pref flipping.
 #ifdef NIGHTLY_BUILD
 pref("extensions.screenshots.system-disabled", false);
 #else
 pref("extensions.screenshots.system-disabled", true);
--- a/browser/extensions/onboarding/README.md
+++ b/browser/extensions/onboarding/README.md
@@ -9,16 +9,20 @@ Everytime `about:home` or `about:newtab`
 ## Landing rules
 
 We would apply some rules:
 
 * Avoid `chrome://` in `onbaording.js` since onboarding is intented to be injected into a normal content process page.
 * All styles and ids should be formated as `onboarding-*` to avoid conflict with the origin page.
 * All strings in `locales` should be formated as `onboarding.*` for consistency.
 
+## How to change the order of tours
+
+Edit `browser/app/profile/firefox.js` and modify `browser.onboarding.newtour` for the new user tour or `browser.onboarding.updatetour` for the update user tour. You can change the tour list and the order by concate `tourIds` with `,` sign. You can find available `tourId` from `onboardingTourset` in `onboarding.js`.
+
 ## How to pump tour set version after update tours
 
 The tourset version is used to track the last major tourset change version. The `tourset-version` pref store the major tourset version (ex: `1`) but not the current browser version. When browser update to the next version (ex: 58, 59) the tourset pref is still `1` if we didn't do any major tourset update.
 
 Once the tour set version is updated (ex: `2`), onboarding overlay should show the update tour to the updated user (ex: update from v56 -> v57), even when user have watched the previous tours or preferred to hide the previous tours.
 
 Edit `browser/app/profile/firefox.js` and set `browser.onboarding.tourset-version` as `[tourset version]` (in integer format).
 
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -17,17 +17,17 @@ const BUNDLE_URI = "chrome://onboarding/
 const UITOUR_JS_URI = "resource://onboarding/lib/UITour-lib.js";
 const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
 const BRAND_SHORT_NAME = Services.strings
                      .createBundle("chrome://branding/locale/brand.properties")
                      .GetStringFromName("brandShortName");
 
 /**
  * Add any number of tours, following the format
- * {
+ * "tourId": { // The short tour id which could be saved in pref
  *   // The unique tour id
  *   id: "onboarding-tour-addons",
  *   // The string id of tour name which would be displayed on the navigation bar
  *   tourNameId: "onboarding.tour-addon",
  *   // The method returing strings used on tour notification
  *   getNotificationStrings(bundle):
  *     - title: // The string of tour notification title
  *     - message: // The string of tour notification message
@@ -35,18 +35,18 @@ const BRAND_SHORT_NAME = Services.string
  *   // Return a div appended with elements for this tours.
  *   // Each tour should contain the following 3 sections in the div:
  *   // .onboarding-tour-description, .onboarding-tour-content, .onboarding-tour-button-container.
  *   // Add onboarding-no-button css class in the div if this tour does not need a button container.
  *   // If there was a .onboarding-tour-action-button present and was clicked, tour would be marked as completed.
  *   getPage() {},
  * },
  **/
-var onboardingTours = [
-  {
+var onboardingTourset = {
+  "private": {
     id: "onboarding-tour-private-browsing",
     tourNameId: "onboarding.tour-private-browsing",
     getNotificationStrings(bundle) {
       return {
         title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-private-browsing.title"),
         message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-private-browsing.message"),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
@@ -63,17 +63,17 @@ var onboardingTours = [
         </section>
         <aside class="onboarding-tour-button-container">
           <button id="onboarding-tour-private-browsing-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-private-browsing.button"></button>
         </aside>
       `;
       return div;
     },
   },
-  {
+  "addons": {
     id: "onboarding-tour-addons",
     tourNameId: "onboarding.tour-addons",
     getNotificationStrings(bundle) {
       return {
         title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-addons.title"),
         message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-addons.message", [BRAND_SHORT_NAME], 1),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
@@ -90,17 +90,17 @@ var onboardingTours = [
         </section>
         <aside class="onboarding-tour-button-container">
           <button id="onboarding-tour-addons-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-addons.button"></button>
         </aside>
       `;
       return div;
     },
   },
-  {
+  "customize": {
     id: "onboarding-tour-customize",
     tourNameId: "onboarding.tour-customize",
     getNotificationStrings(bundle) {
       return {
         title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-customize.title"),
         message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-customize.message", [BRAND_SHORT_NAME], 1),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
@@ -117,17 +117,17 @@ var onboardingTours = [
         </section>
         <aside class="onboarding-tour-button-container">
           <button id="onboarding-tour-customize-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-customize.button"></button>
         </aside>
       `;
       return div;
     },
   },
-  {
+  "search": {
     id: "onboarding-tour-search",
     tourNameId: "onboarding.tour-search2",
     getNotificationStrings(bundle) {
       return {
         title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-search.title"),
         message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-search.message"),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
@@ -144,17 +144,17 @@ var onboardingTours = [
         </section>
         <aside class="onboarding-tour-button-container">
           <button id="onboarding-tour-search-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-search.button"></button>
         </aside>
       `;
       return div;
     },
   },
-  {
+  "default": {
     id: "onboarding-tour-default-browser",
     tourNameId: "onboarding.tour-default-browser",
     getNotificationStrings(bundle) {
       return {
         title: bundle.formatStringFromName("onboarding.notification.onboarding-tour-default-browser.title", [BRAND_SHORT_NAME], 1),
         message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-default-browser.message", [BRAND_SHORT_NAME], 1),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
@@ -174,17 +174,17 @@ var onboardingTours = [
         </section>
         <aside class="onboarding-tour-button-container">
           <button id="onboarding-tour-default-browser-button" class="onboarding-tour-action-button" data-l10n-id="${defaultBrowserButtonId}"></button>
         </aside>
       `;
       return div;
     },
   },
-  {
+  "sync": {
     id: "onboarding-tour-sync",
     tourNameId: "onboarding.tour-sync2",
     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"),
       };
@@ -207,34 +207,41 @@ var onboardingTours = [
           <img src="resource://onboarding/img/figure_sync.svg" />
         </section>
       `;
       div.querySelector("#onboarding-tour-sync-email-input").placeholder =
         bundle.GetStringFromName("onboarding.tour-sync.email-input.placeholder");
       return div;
     },
   },
-];
+};
 
 /**
  * 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);
   }
 
   async init(contentWindow) {
     this._window = contentWindow;
     this._tourItems = [];
     this._tourPages = [];
+    this._tours = [];
 
-    // we only support the new user tour at this moment
-    if (Services.prefs.getStringPref("browser.onboarding.tour-type", "update") !== "new") {
+    let tourIds = this._getTourIDList(Services.prefs.getStringPref("browser.onboarding.tour-type", "update"));
+    tourIds.forEach(tourId => {
+      if (onboardingTourset[tourId]) {
+        this._tours.push(onboardingTourset[tourId]);
+      }
+    });
+
+    if (this._tours.length === 0) {
       return;
     }
 
     // We want to create and append elements after CSS is loaded so
     // no flash of style changes and no additional reflow.
     await this._loadCSS();
     this._bundle = Services.strings.createBundle(BUNDLE_URI);
 
@@ -251,16 +258,21 @@ class Onboarding {
     // Destroy on unload. This is to ensure we remove all the stuff we left.
     // No any leak out there.
     this._window.addEventListener("unload", () => this.destroy());
 
     this._initPrefObserver();
     this._initNotification();
   }
 
+  _getTourIDList(tourType) {
+    let tours = Services.prefs.getStringPref(`browser.onboarding.${tourType}tour`, "");
+    return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim());
+  }
+
   _initNotification() {
     let doc = this._window.document;
     if (doc.hidden) {
       // When the preloaded-browser feature is on,
       // it would preload an hidden about:newtab in the background.
       // We don't want to show notification in that hidden state.
       let onVisible = () => {
         if (!doc.hidden) {
@@ -280,17 +292,17 @@ class Onboarding {
     }
 
     this._prefsObserved = new Map();
     this._prefsObserved.set("browser.onboarding.hidden", prefValue => {
       if (prefValue) {
         this.destroy();
       }
     });
-    onboardingTours.forEach(tour => {
+    this._tours.forEach(tour => {
       let tourId = tour.id;
       this._prefsObserved.set(`browser.onboarding.tour.${tourId}.completed`, () => {
         this.markTourCompletionState(tourId);
       });
     });
     for (let [name, callback] of this._prefsObserved) {
       Preferences.observe(name, callback);
     }
@@ -350,17 +362,17 @@ class Onboarding {
     if (this._notificationBar) {
       this._notificationBar.remove();
     }
   }
 
   toggleOverlay() {
     if (this._tourItems.length == 0) {
       // Lazy loading until first toggle.
-      this._loadTours(onboardingTours);
+      this._loadTours(this._tours);
     }
 
     this.hideNotification();
     this._overlay.classList.toggle("onboarding-opened");
 
     let hiddenCheckbox = this._window.document.getElementById("onboarding-tour-hidden-checkbox");
     if (hiddenCheckbox.checked) {
       this.hide();
@@ -413,34 +425,34 @@ class Onboarding {
       return;
     }
 
     // Pick out the next target tour to show
     let targetTour = null;
 
     // Take the last tour as the default last prompted
     // so below would start from the 1st one if found no the last prompted from the pref.
-    let lastPromptedId = onboardingTours[onboardingTours.length - 1].id;
+    let lastPromptedId = this._tours[this._tours.length - 1].id;
     lastPromptedId = Preferences.get("browser.onboarding.notification.lastPrompted", lastPromptedId);
 
-    let lastTourIndex = onboardingTours.findIndex(tour => tour.id == lastPromptedId);
+    let lastTourIndex = this._tours.findIndex(tour => tour.id == lastPromptedId);
     if (lastTourIndex < 0) {
       // Couldn't find the tour.
       // This could be because the pref was manually modified into unknown value
       // or the tour version has been updated so have an new tours set.
       // Take the last tour as the last prompted so would start from the 1st one below.
-      lastTourIndex = onboardingTours.length - 1;
+      lastTourIndex = this._tours.length - 1;
     }
 
     // Form tours to notify into the order we want.
     // For example, There are tour #0 ~ #5 and the #3 is the last prompted.
     // This would form [#4, #5, #0, #1, #2, #3].
     // So the 1st met incomplete tour in #4 ~ #2 would be the one to show.
     // Or #3 would be the one to show if #4 ~ #2 are all completed.
-    let toursToNotify = [ ...onboardingTours.slice(lastTourIndex + 1), ...onboardingTours.slice(0, lastTourIndex + 1) ];
+    let toursToNotify = [ ...this._tours.slice(lastTourIndex + 1), ...this._tours.slice(0, lastTourIndex + 1) ];
     targetTour = toursToNotify.find(tour => !this.isTourCompleted(tour.id));
 
 
     if (!targetTour) {
       this.sendMessageToChrome("set-prefs", [{
         name: "browser.onboarding.notification.finished",
         value: true
       }]);
@@ -502,17 +514,17 @@ class Onboarding {
       <button id="onboarding-notification-close-btn"></button>
     `;
     let toolTip = this._bundle.formatStringFromName("onboarding.notification-icon-tool-tip", [BRAND_SHORT_NAME], 1);
     div.querySelector("#onboarding-notification-icon").setAttribute("data-tooltip", toolTip);
     return div;
   }
 
   hide() {
-    this.setToursCompleted(onboardingTours.map(tour => tour.id));
+    this.setToursCompleted(this._tours.map(tour => tour.id));
     this.sendMessageToChrome("set-prefs", [
       {
         name: "browser.onboarding.hidden",
         value: true
       },
       {
         name: "browser.onboarding.notification.finished",
         value: true