Bug 1083466 - Add a button to the Loop panel for the Getting Started tour. r=mikedeboer a=lsblakk
authorJared Wein <jwein@mozilla.com>
Tue, 18 Nov 2014 12:39:29 -0500
changeset 233971 c0bc86e8c00095a2992971274305cf903b7e2099
parent 233970 107c64f93febe8a9ae93c7f8ec49483618b74f3b
child 233972 9d8d0c858698f4486ad1e47e49660ce0ef4e00fd
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer, lsblakk
bugs1083466
milestone35.0a2
Bug 1083466 - Add a button to the Loop panel for the Getting Started tour. r=mikedeboer a=lsblakk
browser/app/profile/firefox.js
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/shared/css/panel.css
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/test/desktop-local/panel_test.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1637,16 +1637,18 @@ pref("loop.throttled2", false);
 pref("loop.enabled", true);
 pref("loop.throttled2", true);
 pref("loop.soft_start_ticket_number", -1);
 pref("loop.soft_start_hostname", "soft-start.loop.services.mozilla.com");
 #endif
 
 pref("loop.server", "https://loop.services.mozilla.com");
 pref("loop.seenToS", "unseen");
+pref("loop.gettingStarted.seen", false);
+pref("loop.gettingStarted.url", "https://bugzilla.mozilla.org/show_bug.cgi?id=1099462");
 pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
 pref("loop.legal.ToS_url", "https://hello.firefox.com/legal/terms/");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/ringtone.ogg");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -447,16 +447,33 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function(prefName) {
         return MozLoopService.getLoopCharPref(prefName);
       }
     },
 
     /**
+     * Set any boolean preference under "loop."
+     *
+     * @param {String} prefName The name of the pref without the preceding "loop."
+     * @param {bool} value The value to set.
+     *
+     * Any errors thrown by the Mozilla pref API are logged to the console
+     * and cause false to be returned.
+     */
+    setLoopBoolPref: {
+      enumerable: true,
+      writable: true,
+      value: function(prefName, value) {
+        MozLoopService.setLoopBoolPref(prefName, value);
+      }
+    },
+
+    /**
      * Return any preference under "loop." that's coercible to a boolean
      * preference.
      *
      * @param {String} prefName The name of the pref without the preceding
      * "loop."
      *
      * Any errors thrown by the Mozilla pref API are logged to the console
      * and cause null to be returned. This includes the case of the preference
@@ -592,16 +609,27 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.openFxASettings();
       },
     },
 
     /**
+     * Opens the Getting Started tour in the browser.
+     */
+    openGettingStartedTour: {
+      enumerable: true,
+      writable: true,
+      value: function() {
+        return MozLoopService.openGettingStartedTour();
+      },
+    },
+
+    /**
      * Copies passed string onto the system clipboard.
      *
      * @param {String} str The string to copy
      */
     copyString: {
       enumerable: true,
       writable: true,
       value: function(str) {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1351,16 +1351,33 @@ this.MozLoopService = {
     } catch (ex) {
       log.error("getLoopCharPref had trouble getting " + prefName +
         "; exception: " + ex);
       return null;
     }
   },
 
   /**
+   * Set any boolean preference under "loop.".
+   *
+   * @param {String} prefName The name of the pref without the preceding "loop."
+   * @param {boolean} value The value to set.
+   *
+   * Any errors thrown by the Mozilla pref API are logged to the console.
+   */
+  setLoopBoolPref: function(prefName, value) {
+    try {
+      Services.prefs.setBoolPref("loop." + prefName, value);
+    } catch (ex) {
+      log.error("setLoopCharPref had trouble setting " + prefName +
+        "; exception: " + ex);
+    }
+  },
+
+  /**
    * Return any preference under "loop." that's coercible to a character
    * preference.
    *
    * @param {String} prefName The name of the pref without the preceding
    * "loop."
    *
    * Any errors thrown by the Mozilla pref API are logged to the console
    * and cause null to be returned. This includes the case of the preference
@@ -1479,16 +1496,29 @@ this.MozLoopService = {
       let win = Services.wm.getMostRecentWindow("navigator:browser");
       win.switchToTabHavingURI(url.toString(), true);
     } catch (ex) {
       log.error("Error opening FxA settings", ex);
     }
   }),
 
   /**
+   * Opens the Getting Started tour in the browser.
+   */
+  openGettingStartedTour: Task.async(function() {
+    try {
+      let url = Services.prefs.getCharPref("loop.gettingStarted.url");
+      let win = Services.wm.getMostRecentWindow("navigator:browser");
+      win.switchToTabHavingURI(url, true);
+    } catch (ex) {
+      log.error("Error opening Getting Started tour", ex);
+    }
+  }),
+
+  /**
    * Performs a hawk based request to the loop server.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
    *                                        One of the LOOP_SESSION_TYPE members.
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
    * @param {Object} payloadObj An object which is converted to JSON and
    *                            transmitted with the request.
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -160,16 +160,44 @@ loop.panel = (function(_, mozL10n) {
               React.DOM.span(null, __("display_name_dnd_status"))
             )
           )
         )
       );
     }
   });
 
+  var GettingStartedView = React.createClass({displayName: 'GettingStartedView',
+    componentDidMount: function() {
+      navigator.mozLoop.setLoopBoolPref("gettingStarted.seen", true);
+    },
+
+    handleButtonClick: function() {
+      navigator.mozLoop.openGettingStartedTour();
+    },
+
+    render: function() {
+      if (navigator.mozLoop.getLoopBoolPref("gettingStarted.seen")) {
+        return null;
+      }
+      return (
+        React.DOM.div({id: "fte-getstarted"}, 
+          React.DOM.header({id: "fte-title"}, 
+            mozL10n.get("first_time_experience_title", {
+              "clientShortname": mozL10n.get("clientShortname2")
+            })
+          ), 
+          Button({htmlId: "fte-button", 
+                  onClick: this.handleButtonClick, 
+                  caption: mozL10n.get("first_time_experience_button_label")})
+        )
+      );
+    }
+  });
+
   var ToSView = React.createClass({displayName: 'ToSView',
     getInitialState: function() {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
@@ -402,17 +430,17 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
       return (
         React.DOM.div({className: "generate-url"}, 
-          React.DOM.header(null, __("share_link_header_text")), 
+          React.DOM.header({id: "share-link-header"}, mozL10n.get("share_link_header_text")), 
           React.DOM.div({className: "generate-url-stack"}, 
             React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
                    onCopy: this.handleLinkExfiltration, 
                    className: cx({"generate-url-input": true,
                                   pending: this.state.pending,
                                   // Used in functional testing, signals that
                                   // call url was received from loop server
                                   callUrl: !this.state.pending})}), 
@@ -704,27 +732,29 @@ loop.panel = (function(_, mozL10n) {
      * The rooms feature is hidden by default for now. Once it gets mainstream,
      * this method can be simplified.
      */
     _renderRoomsOrCallTab: function() {
       if (!this._roomsEnabled()) {
         return (
           Tab({name: "call"}, 
             React.DOM.div({className: "content-area"}, 
+              GettingStartedView(null), 
               CallUrlResult({client: this.props.client, 
                              notifications: this.props.notifications, 
                              callUrl: this.props.callUrl}), 
               ToSView(null)
             )
           )
         );
       }
 
       return (
         Tab({name: "rooms"}, 
+          GettingStartedView(null), 
           RoomList({dispatcher: this.props.dispatcher, 
                     store: this.props.roomStore, 
                     userDisplayName: this._getUserDisplayName()}), 
           ToSView(null)
         )
       );
     },
 
@@ -825,21 +855,22 @@ loop.panel = (function(_, mozL10n) {
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
-    UserIdentity: UserIdentity,
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
+    GettingStartedView: GettingStartedView,
     PanelView: PanelView,
     RoomEntry: RoomEntry,
     RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
-    ToSView: ToSView
+    ToSView: ToSView,
+    UserIdentity: UserIdentity,
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -160,16 +160,44 @@ loop.panel = (function(_, mozL10n) {
               <span>{__("display_name_dnd_status")}</span>
             </li>
           </ul>
         </div>
       );
     }
   });
 
+  var GettingStartedView = React.createClass({
+    componentDidMount: function() {
+      navigator.mozLoop.setLoopBoolPref("gettingStarted.seen", true);
+    },
+
+    handleButtonClick: function() {
+      navigator.mozLoop.openGettingStartedTour();
+    },
+
+    render: function() {
+      if (navigator.mozLoop.getLoopBoolPref("gettingStarted.seen")) {
+        return null;
+      }
+      return (
+        <div id="fte-getstarted">
+          <header id="fte-title">
+            {mozL10n.get("first_time_experience_title", {
+              "clientShortname": mozL10n.get("clientShortname2")
+            })}
+          </header>
+          <Button htmlId="fte-button"
+                  onClick={this.handleButtonClick}
+                  caption={mozL10n.get("first_time_experience_button_label")} />
+        </div>
+      );
+    }
+  });
+
   var ToSView = React.createClass({
     getInitialState: function() {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
@@ -402,17 +430,17 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
       return (
         <div className="generate-url">
-          <header>{__("share_link_header_text")}</header>
+          <header id="share-link-header">{mozL10n.get("share_link_header_text")}</header>
           <div className="generate-url-stack">
             <input type="url" value={this.state.callUrl} readOnly="true"
                    onCopy={this.handleLinkExfiltration}
                    className={cx({"generate-url-input": true,
                                   pending: this.state.pending,
                                   // Used in functional testing, signals that
                                   // call url was received from loop server
                                   callUrl: !this.state.pending})} />
@@ -704,27 +732,29 @@ loop.panel = (function(_, mozL10n) {
      * The rooms feature is hidden by default for now. Once it gets mainstream,
      * this method can be simplified.
      */
     _renderRoomsOrCallTab: function() {
       if (!this._roomsEnabled()) {
         return (
           <Tab name="call">
             <div className="content-area">
+              <GettingStartedView />
               <CallUrlResult client={this.props.client}
                              notifications={this.props.notifications}
                              callUrl={this.props.callUrl} />
               <ToSView />
             </div>
           </Tab>
         );
       }
 
       return (
         <Tab name="rooms">
+          <GettingStartedView />
           <RoomList dispatcher={this.props.dispatcher}
                     store={this.props.roomStore}
                     userDisplayName={this._getUserDisplayName()}/>
           <ToSView />
         </Tab>
       );
     },
 
@@ -825,21 +855,22 @@ loop.panel = (function(_, mozL10n) {
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
-    UserIdentity: UserIdentity,
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
+    GettingStartedView: GettingStartedView,
     PanelView: PanelView,
     RoomEntry: RoomEntry,
     RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
-    ToSView: ToSView
+    ToSView: ToSView,
+    UserIdentity: UserIdentity,
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -97,20 +97,46 @@ body {
 /* Content area and input fields */
 
 .content-area {
   padding: 14px;
 }
 
 .content-area header {
   font-weight: 700;
+}
+
+#fte-getstarted {
+  padding-top: 1em;
+  padding-bottom: 1em;
+  border-bottom: 1px solid #ccc;
+  margin-bottom: 1em;
+}
+
+#fte-title {
+  text-align: center;
+  margin-bottom: .5em;
+}
+
+#fte-button {
+  width: 50%;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+  font-size: 1rem;
+  padding: .5rem 1rem;
+  height: auto; /* Needed to override .button's height:26px; */
+}
+
+/* Need to remove when these rules when the Beta tag is removed */
+#share-link-header {
   -moz-padding-start: 20px;
 }
-
-.tab-view + .tab .content-area header {
+#fte-getstarted + .generate-url > #share-link-header,
+.tab-view + .tab .content-area > .generate-url > #share-link-header {
   /* The header shouldn't be indented if the tabs are present. */
   -moz-padding-start: 0;
 }
 
 .content-area label {
   display: block;
   width: 100%;
   margin-top: 10px;
@@ -429,16 +455,17 @@ body[dir=rtl] .dropdown-menu-item {
   right: 4px;
 }
 
 body[dir=rtl] .generate-url-spinner {
   left: 4px;
   right: auto;
 }
 
+#fte-button,
 .generate-url .button {
   background-color: #0096dd;
   border-color: #0096dd;
   color: #fff;
 }
 
 .generate-url .button:hover {
   background-color: #008acb;
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -713,34 +713,37 @@ loop.shared.views = (function(_, OT, l10
   });
 
   var Button = React.createClass({displayName: 'Button',
     propTypes: {
       caption: React.PropTypes.string.isRequired,
       onClick: React.PropTypes.func.isRequired,
       disabled: React.PropTypes.bool,
       additionalClass: React.PropTypes.string,
+      htmlId: React.PropTypes.string,
     },
 
     getDefaultProps: function() {
       return {
         disabled: false,
         additionalClass: "",
+        htmlId: "",
       };
     },
 
     render: function() {
       var cx = React.addons.classSet;
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         React.DOM.button({onClick: this.props.onClick, 
                 disabled: this.props.disabled, 
+                id: this.props.htmlId, 
                 className: cx(classObject)}, 
           React.DOM.span({className: "button-caption"}, this.props.caption), 
           this.props.children
         )
       )
     }
   });
 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -713,34 +713,37 @@ loop.shared.views = (function(_, OT, l10
   });
 
   var Button = React.createClass({
     propTypes: {
       caption: React.PropTypes.string.isRequired,
       onClick: React.PropTypes.func.isRequired,
       disabled: React.PropTypes.bool,
       additionalClass: React.PropTypes.string,
+      htmlId: React.PropTypes.string,
     },
 
     getDefaultProps: function() {
       return {
         disabled: false,
         additionalClass: "",
+        htmlId: "",
       };
     },
 
     render: function() {
       var cx = React.addons.classSet;
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         <button onClick={this.props.onClick}
                 disabled={this.props.disabled}
+                id={this.props.htmlId}
                 className={cx(classObject)}>
           <span className="button-caption">{this.props.caption}</span>
           {this.props.children}
         </button>
       )
     }
   });
 
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -29,19 +29,20 @@ describe("loop.panel", function() {
       doNotDisturb: true,
       fxAEnabled: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
-      getLoopBoolPref: sandbox.stub(),
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub().returns("unseen"),
+      getLoopBoolPref: sandbox.stub(),
+      setLoopBoolPref: sandbox.stub(),
       getPluralForm: function() {
         return "fakeText";
       },
       copyString: sandbox.stub(),
       noteCallUrlExpiry: sinon.spy(),
       composeEmail: sinon.spy(),
       telemetryAdd: sinon.spy(),
       contacts: {
@@ -356,16 +357,47 @@ describe("loop.panel", function() {
     });
 
     describe("#render", function() {
       it("should render a ToSView", function() {
         var view = createTestPanelView();
 
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
       });
+
+      it("should not render a ToSView when the view has been 'seen'", function() {
+        navigator.mozLoop.getLoopCharPref = function() {
+          return "seen";
+        };
+        var view = createTestPanelView();
+
+        try {
+          TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
+          sinon.assert.fail("Should not find the ToSView if it has been 'seen'");
+        } catch (ex) {}
+      });
+
+      it("should render a GettingStarted view", function() {
+        var view = createTestPanelView();
+
+        TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView);
+      });
+
+      it("should not render a GettingStartedView when the view has been seen", function() {
+        navigator.mozLoop.getLoopBoolPref = function() {
+          return true;
+        };
+        var view = createTestPanelView();
+
+        try {
+          TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView);
+          sinon.assert.fail("Should not find the GettingStartedView if it has been seen");
+        } catch (ex) {}
+      });
+
     });
   });
 
   describe("loop.panel.CallUrlResult", function() {
     var fakeClient, callUrlData, view;
 
     beforeEach(function() {
       callUrlData = {