Bug 1153788 - Part 2. Ask the user to re-sign in to Loop if they don't have encryption keys for FxA. r=mikedeboer
authorMark Banner <standard8@mozilla.com>
Fri, 08 May 2015 13:46:52 +0100
changeset 274488 8fcc515496915e9a8e1eec4979921c0b2a1c7381
parent 274487 6081ec5c83dc70998a24cadd758af3af60363f32
child 274489 77dd978b8ef378943d846f745683cee4bca363e7
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1153788
milestone40.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 1153788 - Part 2. Ask the user to re-sign in to Loop if they don't have encryption keys for FxA. r=mikedeboer
browser/components/loop/content/css/panel.css
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/modules/MozLoopAPI.jsm
browser/components/loop/modules/MozLoopService.jsm
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
browser/components/loop/ui/fake-mozLoop.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
browser/components/uitour/test/browser_UITour_loop.js
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -18,16 +18,51 @@ body {
 .panel .messages {
   margin: 0;
 }
 
 .panel .messages .alert {
   margin: 0;
 }
 
+/* Sign-in request view */
+
+.sign-in-request {
+  text-align: center;
+  vertical-align: middle;
+  margin: 2em 0;
+}
+
+.sign-in-request > h1 {
+  font-size: 1.7em;
+  margin-bottom: .2em;
+}
+
+.sign-in-request > h2,
+.sign-in-request > a {
+  font-size: 1.2em;
+}
+
+.sign-in-request > a {
+  cursor: pointer;
+  color: #0295df;
+}
+
+.sign-in-request > a:hover:active {
+  text-decoration: underline;
+}
+
+.sign-in-request-button {
+  font-size: 1rem;
+  margin: 1rem;
+  width: 80%;
+  padding: .5rem 1rem;
+  border-radius: 3px;
+}
+
 /* Tabs and tab selection buttons */
 
 .tab-view-container {
   background-image: url("../shared/img/beta-ribbon.svg#beta-ribbon");
   background-size: 36px 36px;
   background-repeat: no-repeat;
 }
 
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -209,16 +209,55 @@ loop.panel = (function(_, mozL10n) {
           React.createElement(Button, {htmlId: "fte-button", 
                   onClick: this.handleButtonClick, 
                   caption: mozL10n.get("first_time_experience_button_label")})
         )
       );
     }
   });
 
+  /**
+   * Displays a view requesting the user to sign-in again.
+   */
+  var SignInRequestView = React.createClass({displayName: "SignInRequestView",
+    mixins: [sharedMixins.WindowCloseMixin],
+
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired
+    },
+
+    handleSignInClick: function(event) {
+      event.preventDefault();
+      this.props.mozLoop.logInToFxA(true);
+      this.closeWindow();
+    },
+
+    handleGuestClick: function(event) {
+      this.props.mozLoop.logOutFromFxA();
+    },
+
+    render: function() {
+      return (
+        React.createElement("div", {className: "sign-in-request"}, 
+          React.createElement("h1", null, mozL10n.get("sign_in_again_title_line_one")), 
+          React.createElement("h2", null, mozL10n.get("sign_in_again_title_line_two")), 
+          React.createElement("div", null, 
+            React.createElement("button", {className: "btn btn-info sign-in-request-button", 
+                    onClick: this.handleSignInClick}, 
+              mozL10n.get("sign_in_again_button")
+            )
+          ), 
+          React.createElement("a", {onClick: this.handleGuestClick}, 
+            mozL10n.get("sign_in_again_use_as_guest_button")
+          )
+        )
+      );
+    }
+  });
+
   var ToSView = React.createClass({displayName: "ToSView",
     mixins: [sharedMixins.WindowCloseMixin],
 
     getInitialState: function() {
       var getPref = navigator.mozLoop.getLoopPref.bind(navigator.mozLoop);
 
       return {
         seenToS: getPref("seenToS"),
@@ -753,16 +792,17 @@ loop.panel = (function(_, mozL10n) {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       roomStore:
         React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
+        hasEncryptionKey: this.props.mozLoop.hasEncryptionKey,
         userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
         gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
       };
     },
 
     _serviceErrorToShow: function() {
       if (!this.props.mozLoop.errors ||
           !Object.keys(this.props.mozLoop.errors).length) {
@@ -791,17 +831,20 @@ loop.panel = (function(_, mozL10n) {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
     _onStatusChanged: function() {
       var profile = this.props.mozLoop.userProfile;
       var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
       var newUid = profile ? profile.uid : null;
-      if (currUid != newUid) {
+      if (currUid == newUid) {
+        // Update the state of hasEncryptionKey as this might have changed now.
+        this.setState({hasEncryptionKey: this.props.mozLoop.hasEncryptionKey});
+      } else {
         // On profile change (login, logout), switch back to the default tab.
         this.selectTab("rooms");
         this.setState({userProfile: profile});
       }
       this.updateServiceErrors();
     },
 
     _gettingStartedSeen: function() {
@@ -822,17 +865,21 @@ loop.panel = (function(_, mozL10n) {
     },
 
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
 
     selectTab: function(name) {
-      this.refs.tabView.setState({ selectedTab: name });
+      // The tab view might not be created yet (e.g. getting started or fxa
+      // re-sign in.
+      if (this.refs.tabView) {
+        this.refs.tabView.setState({ selectedTab: name });
+      }
     },
 
     componentWillMount: function() {
       this.updateServiceErrors();
     },
 
     componentDidMount: function() {
       window.addEventListener("LoopStatusChanged", this._onStatusChanged);
@@ -860,16 +907,20 @@ loop.panel = (function(_, mozL10n) {
             React.createElement(NotificationListView, {notifications: this.props.notifications, 
                                   clearOnDocumentHidden: true}), 
             React.createElement(GettingStartedView, null), 
             React.createElement(ToSView, null)
           )
         );
       }
 
+      if (!this.state.hasEncryptionKey) {
+        return React.createElement(SignInRequestView, {mozLoop: this.props.mozLoop});
+      }
+
       // Determine which buttons to NOT show.
       var hideButtons = [];
       if (!this.state.userProfile && !this.props.showTabButtons) {
         hideButtons.push("contacts");
       }
 
       return (
         React.createElement("div", null, 
@@ -932,18 +983,17 @@ loop.panel = (function(_, mozL10n) {
       mozLoop: navigator.mozLoop,
       notifications: notifications
     });
 
     React.render(React.createElement(PanelView, {
       notifications: notifications, 
       roomStore: roomStore, 
       mozLoop: navigator.mozLoop, 
-      dispatcher: dispatcher}
-    ), document.querySelector("#main"));
+      dispatcher: dispatcher}), document.querySelector("#main"));
 
     document.body.setAttribute("dir", mozL10n.getDirection());
     document.body.setAttribute("platform", loop.shared.utils.getPlatform());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
@@ -954,14 +1004,15 @@ loop.panel = (function(_, mozL10n) {
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     GettingStartedView: GettingStartedView,
     NewRoomView: NewRoomView,
     PanelView: PanelView,
     RoomEntry: RoomEntry,
     RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
+    SignInRequestView: SignInRequestView,
     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
@@ -209,16 +209,55 @@ loop.panel = (function(_, mozL10n) {
           <Button htmlId="fte-button"
                   onClick={this.handleButtonClick}
                   caption={mozL10n.get("first_time_experience_button_label")} />
         </div>
       );
     }
   });
 
+  /**
+   * Displays a view requesting the user to sign-in again.
+   */
+  var SignInRequestView = React.createClass({
+    mixins: [sharedMixins.WindowCloseMixin],
+
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired
+    },
+
+    handleSignInClick: function(event) {
+      event.preventDefault();
+      this.props.mozLoop.logInToFxA(true);
+      this.closeWindow();
+    },
+
+    handleGuestClick: function(event) {
+      this.props.mozLoop.logOutFromFxA();
+    },
+
+    render: function() {
+      return (
+        <div className="sign-in-request">
+          <h1>{mozL10n.get("sign_in_again_title_line_one")}</h1>
+          <h2>{mozL10n.get("sign_in_again_title_line_two")}</h2>
+          <div>
+            <button className="btn btn-info sign-in-request-button"
+                    onClick={this.handleSignInClick}>
+              {mozL10n.get("sign_in_again_button")}
+            </button>
+          </div>
+          <a onClick={this.handleGuestClick}>
+            {mozL10n.get("sign_in_again_use_as_guest_button")}
+          </a>
+        </div>
+      );
+    }
+  });
+
   var ToSView = React.createClass({
     mixins: [sharedMixins.WindowCloseMixin],
 
     getInitialState: function() {
       var getPref = navigator.mozLoop.getLoopPref.bind(navigator.mozLoop);
 
       return {
         seenToS: getPref("seenToS"),
@@ -753,16 +792,17 @@ loop.panel = (function(_, mozL10n) {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       roomStore:
         React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
+        hasEncryptionKey: this.props.mozLoop.hasEncryptionKey,
         userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
         gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
       };
     },
 
     _serviceErrorToShow: function() {
       if (!this.props.mozLoop.errors ||
           !Object.keys(this.props.mozLoop.errors).length) {
@@ -791,17 +831,20 @@ loop.panel = (function(_, mozL10n) {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
     _onStatusChanged: function() {
       var profile = this.props.mozLoop.userProfile;
       var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
       var newUid = profile ? profile.uid : null;
-      if (currUid != newUid) {
+      if (currUid == newUid) {
+        // Update the state of hasEncryptionKey as this might have changed now.
+        this.setState({hasEncryptionKey: this.props.mozLoop.hasEncryptionKey});
+      } else {
         // On profile change (login, logout), switch back to the default tab.
         this.selectTab("rooms");
         this.setState({userProfile: profile});
       }
       this.updateServiceErrors();
     },
 
     _gettingStartedSeen: function() {
@@ -822,17 +865,21 @@ loop.panel = (function(_, mozL10n) {
     },
 
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
 
     selectTab: function(name) {
-      this.refs.tabView.setState({ selectedTab: name });
+      // The tab view might not be created yet (e.g. getting started or fxa
+      // re-sign in.
+      if (this.refs.tabView) {
+        this.refs.tabView.setState({ selectedTab: name });
+      }
     },
 
     componentWillMount: function() {
       this.updateServiceErrors();
     },
 
     componentDidMount: function() {
       window.addEventListener("LoopStatusChanged", this._onStatusChanged);
@@ -860,16 +907,20 @@ loop.panel = (function(_, mozL10n) {
             <NotificationListView notifications={this.props.notifications}
                                   clearOnDocumentHidden={true} />
             <GettingStartedView />
             <ToSView />
           </div>
         );
       }
 
+      if (!this.state.hasEncryptionKey) {
+        return <SignInRequestView mozLoop={this.props.mozLoop} />;
+      }
+
       // Determine which buttons to NOT show.
       var hideButtons = [];
       if (!this.state.userProfile && !this.props.showTabButtons) {
         hideButtons.push("contacts");
       }
 
       return (
         <div>
@@ -932,18 +983,17 @@ loop.panel = (function(_, mozL10n) {
       mozLoop: navigator.mozLoop,
       notifications: notifications
     });
 
     React.render(<PanelView
       notifications={notifications}
       roomStore={roomStore}
       mozLoop={navigator.mozLoop}
-      dispatcher={dispatcher}
-    />, document.querySelector("#main"));
+      dispatcher={dispatcher} />, document.querySelector("#main"));
 
     document.body.setAttribute("dir", mozL10n.getDirection());
     document.body.setAttribute("platform", loop.shared.utils.getPlatform());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
@@ -954,14 +1004,15 @@ loop.panel = (function(_, mozL10n) {
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     GettingStartedView: GettingStartedView,
     NewRoomView: NewRoomView,
     PanelView: PanelView,
     RoomEntry: RoomEntry,
     RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
+    SignInRequestView: SignInRequestView,
     ToSView: ToSView,
     UserIdentity: UserIdentity
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/modules/MozLoopAPI.jsm
+++ b/browser/components/loop/modules/MozLoopAPI.jsm
@@ -664,21 +664,30 @@ function injectLoopAPI(targetWindow) {
 
     fxAEnabled: {
       enumerable: true,
       get: function() {
         return MozLoopService.fxAEnabled;
       },
     },
 
+    /**
+     * Start the FxA login flow using the OAuth client and params from the Loop
+     * server.
+     *
+     * @param {Boolean} forceReAuth Set to true to force FxA into a re-auth even
+     *                              if the user is already logged in.
+     * @return {Promise} Returns a promise that is resolved on successful
+     *                   completion, or rejected otherwise.
+     */
     logInToFxA: {
       enumerable: true,
       writable: true,
-      value: function() {
-        return MozLoopService.logInToFxA();
+      value: function(forceReAuth) {
+        return MozLoopService.logInToFxA(forceReAuth);
       }
     },
 
     logOutFromFxA: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.logOutFromFxA();
@@ -689,16 +698,28 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.openFxASettings();
       },
     },
 
     /**
+     * Returns true if this profile has an encryption key.
+     *
+     * @return {Boolean} True if the profile has an encryption key.
+     */
+    hasEncryptionKey: {
+      enumerable: true,
+      get: function() {
+        return MozLoopService.hasEncryptionKey;
+      }
+    },
+
+    /**
      * Opens the Getting Started tour in the browser.
      *
      * @param {String} aSrc
      *   - The UI element that the user used to begin the tour, optional.
      */
     openGettingStartedTour: {
       enumerable: true,
       writable: true,
--- a/browser/components/loop/modules/MozLoopService.jsm
+++ b/browser/components/loop/modules/MozLoopService.jsm
@@ -943,29 +943,34 @@ let MozLoopServiceInternal = {
       return JSON.parse(response.body);
     },
     error => { this._hawkRequestError(error); });
   },
 
   /**
    * Get the OAuth client constructed with Loop OAauth parameters.
    *
+   * @param {Boolean} forceReAuth Set to true to force the user to reauthenticate.
    * @return {Promise}
    */
-  promiseFxAOAuthClient: Task.async(function* () {
+  promiseFxAOAuthClient: Task.async(function* (forceReAuth) {
     // We must make sure to have only a single client otherwise they will have different states and
     // multiple channels. This would happen if the user clicks the Login button more than once.
     if (gFxAOAuthClientPromise) {
       return gFxAOAuthClientPromise;
     }
 
     gFxAOAuthClientPromise = this.promiseFxAOAuthParameters().then(
       parameters => {
         // Add the fact that we want keys to the parameters.
         parameters.keys = true;
+        if (forceReAuth) {
+          parameters.action = "force_auth";
+          parameters.email = MozLoopService.userProfile.email;
+        }
 
         try {
           gFxAOAuthClient = new FxAccountsOAuthClient({
             parameters: parameters
           });
         } catch (ex) {
           gFxAOAuthClientPromise = null;
           throw ex;
@@ -979,21 +984,22 @@ let MozLoopServiceInternal = {
     );
 
     return gFxAOAuthClientPromise;
   }),
 
   /**
    * Get the OAuth client and do the authorization web flow to get an OAuth code.
    *
+   * @param {Boolean} forceReAuth Set to true to force the user to reauthenticate.
    * @return {Promise}
    */
-  promiseFxAOAuthAuthorization: function() {
+  promiseFxAOAuthAuthorization: function(forceReAuth) {
     let deferred = Promise.defer();
-    this.promiseFxAOAuthClient().then(
+    this.promiseFxAOAuthClient(forceReAuth).then(
       client => {
         client.onComplete = this._fxAOAuthComplete.bind(this, deferred);
         client.onError = this._fxAOAuthError.bind(this, deferred);
         client.launchWebFlow();
       },
       error => {
         log.error(error);
         deferred.reject(error);
@@ -1361,16 +1367,28 @@ this.MozLoopService = {
         });
         return;
       }
 
       resolve(MozLoopService.getLoopPref("key"));
     });
   },
 
+  /**
+   * Returns true if this profile has an encryption key. For guest profiles
+   * this is always true, since we can generate a new one if needed. For FxA
+   * profiles, we need to check the preference.
+   *
+   * @return {Boolean} True if the profile has an encryption key.
+   */
+  get hasEncryptionKey() {
+    return !this.userProfile ||
+      Services.prefs.prefHasUserValue("loop.key.fxa");
+  },
+
   get errors() {
     return MozLoopServiceInternal.errors;
   },
 
   get log() {
     return log;
   },
 
@@ -1463,24 +1481,25 @@ this.MozLoopService = {
     }
   },
 
   /**
    * Start the FxA login flow using the OAuth client and params from the Loop server.
    *
    * The caller should be prepared to handle rejections related to network, server or login errors.
    *
+   * @param {Boolean} forceReAuth Set to true to force the user to reauthenticate.
    * @return {Promise} that resolves when the FxA login flow is complete.
    */
-  logInToFxA: function() {
+  logInToFxA: function(forceReAuth) {
     log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
-    if (MozLoopServiceInternal.fxAOAuthTokenData) {
+    if (!forceReAuth && MozLoopServiceInternal.fxAOAuthTokenData) {
       return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
     }
-    return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
+    return MozLoopServiceInternal.promiseFxAOAuthAuthorization(forceReAuth).then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
     }).then(tokenData => {
       MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
       return tokenData;
     }).then(tokenData => {
       return MozLoopServiceInternal.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA).then(() => {
         MozLoopServiceInternal.clearError("login");
         MozLoopServiceInternal.clearError("profile");
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -65,16 +65,19 @@ describe("loop.panel", function() {
       },
       rooms: {
         getAll: function(version, callback) {
           callback(null, []);
         },
         on: sandbox.stub()
       },
       confirm: sandbox.stub(),
+      hasEncryptionKey: true,
+      logInToFxA: sandbox.stub(),
+      logOutFromFxA: sandbox.stub(),
       notifyUITour: sandbox.stub(),
       openURL: sandbox.stub(),
       getSelectedTabMetadata: sandbox.stub()
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
@@ -448,16 +451,32 @@ describe("loop.panel", function() {
         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) {}
       });
 
+      it("should render a SignInRequestView when mozLoop.hasEncryptionKey is false", function() {
+        fakeMozLoop.hasEncryptionKey = false;
+
+        var view = createTestPanelView();
+
+        TestUtils.findRenderedComponentWithType(view, loop.panel.SignInRequestView);
+      });
+
+      it("should render a SignInRequestView when mozLoop.hasEncryptionKey is true", function() {
+        var view = createTestPanelView();
+
+        try {
+          TestUtils.findRenderedComponentWithType(view, loop.panel.SignInRequestView);
+          sinon.assert.fail("Should not find the GettingStartedView if it has been seen");
+        } catch (ex) {}
+      });
     });
   });
 
   describe("loop.panel.RoomEntry", function() {
     var dispatcher, roomData;
 
     beforeEach(function() {
       dispatcher = new loop.Dispatcher();
@@ -925,9 +944,37 @@ describe("loop.panel", function() {
 
          var view = TestUtils.renderIntoDocument(
            React.createElement(loop.panel.ToSView));
 
          expect(view.getDOMNode().querySelector(".powered-by")).eql(null);
        });
 
   });
+
+  describe("loop.panel.SignInRequestView", function() {
+    var view;
+
+    function mountTestComponent() {
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.panel.SignInRequestView, {
+          mozLoop: fakeMozLoop
+        }));
+    }
+
+    it("should call login with forced re-authentication when sign-in is clicked", function() {
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
+
+      sinon.assert.calledOnce(fakeMozLoop.logInToFxA);
+      sinon.assert.calledWithExactly(fakeMozLoop.logInToFxA, true);
+    });
+
+    it("should logout when use as guest is clicked", function() {
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.getDOMNode().querySelector("a"));
+
+      sinon.assert.calledOnce(fakeMozLoop.logOutFromFxA);
+    });
+  });
 });
--- a/browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const kGuestKeyPref = "loop.key";
 const kFxAKeyPref = "loop.key.fxa";
 
 do_register_cleanup(function() {
   Services.prefs.clearUserPref(kGuestKeyPref);
+  Services.prefs.clearUserPref(kFxAKeyPref);
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.fxAOAuthProfile = null;
 });
 
 add_task(function* test_guestCreateKey() {
   // Ensure everything is cleared and we're not logged in.
   Services.prefs.clearUserPref(kGuestKeyPref);
   MozLoopServiceInternal.fxAOAuthTokenData = null;
@@ -53,8 +54,31 @@ add_task(function* test_fxaGetKey() {
   MozLoopServiceInternal.fxAOAuthTokenData = { token_type: "bearer" };
   MozLoopServiceInternal.fxAOAuthProfile = { email: "fake@invalid.com" };
   Services.prefs.clearUserPref(kFxAKeyPref);
 
   // Currently unimplemented, add a test when we implement the code.
   yield Assert.rejects(MozLoopService.promiseProfileEncryptionKey(),
     /not implemented/, "should reject as unimplemented");
 });
+
+add_task(function test_hasEncryptionKey() {
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
+  MozLoopServiceInternal.fxAOAuthProfile = null;
+
+  Services.prefs.clearUserPref(kGuestKeyPref);
+  Services.prefs.clearUserPref(kFxAKeyPref);
+
+  Assert.ok(MozLoopService.hasEncryptionKey, "should return true in guest mode without a key");
+
+  Services.prefs.setCharPref(kGuestKeyPref, "123456");
+
+  Assert.ok(MozLoopService.hasEncryptionKey, "should return true in guest mode with a key");
+
+  MozLoopServiceInternal.fxAOAuthTokenData = { token_type: "bearer" };
+  MozLoopServiceInternal.fxAOAuthProfile = { email: "fake@invalid.com" };
+
+  Assert.ok(!MozLoopService.hasEncryptionKey, "should return false in fxa mode without a key");
+
+  Services.prefs.setCharPref(kFxAKeyPref, "12345678");
+
+  Assert.ok(MozLoopService.hasEncryptionKey, "should return true in fxa mode with a key");
+});
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -127,16 +127,17 @@ navigator.mozLoop = {
       case "gettingStarted.seen":
       case "contacts.gravatars.promo":
       case "contextInConversations.enabled":
         return true;
       case "contacts.gravatars.show":
         return false;
     }
   },
+  hasEncryptionKey: true,
   setLoopPref: function(){},
   releaseCallData: function() {},
   copyString: function() {},
   getUserAvatar: function(emailAddress) {
     return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ?
       "0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40";
   },
   getSelectedTabMetadata: function(callback) {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -12,16 +12,17 @@
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener('DOMContentLoaded', loop.panel.init);
   document.removeEventListener('DOMContentLoaded', loop.conversation.init);
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
+  var SignInRequestView = loop.panel.SignInRequestView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
@@ -260,16 +261,19 @@
   var App = React.createClass({displayName: "App",
     render: function() {
       return (
         React.createElement(ShowCase, null, 
           React.createElement(Section, {name: "PanelView"}, 
             React.createElement("p", {className: "note"}, 
               React.createElement("strong", null, "Note:"), " 332px wide."
             ), 
+            React.createElement(Example, {summary: "Re-sign-in view", dashed: "true", style: {width: "332px"}}, 
+              React.createElement(SignInRequestView, {mozLoop: mockMozLoopRooms})
+            ), 
             React.createElement(Example, {summary: "Room list tab", dashed: "true", style: {width: "332px"}}, 
               React.createElement(PanelView, {client: mockClient, notifications: notifications, 
                          userProfile: {email: "test@example.com"}, 
                          mozLoop: mockMozLoopRooms, 
                          dispatcher: dispatcher, 
                          roomStore: roomStore, 
                          selectedTab: "rooms"})
             ), 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -12,16 +12,17 @@
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener('DOMContentLoaded', loop.panel.init);
   document.removeEventListener('DOMContentLoaded', loop.conversation.init);
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
+  var SignInRequestView = loop.panel.SignInRequestView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
@@ -260,16 +261,19 @@
   var App = React.createClass({
     render: function() {
       return (
         <ShowCase>
           <Section name="PanelView">
             <p className="note">
               <strong>Note:</strong> 332px wide.
             </p>
+            <Example summary="Re-sign-in view" dashed="true" style={{width: "332px"}}>
+              <SignInRequestView mozLoop={mockMozLoopRooms} />
+            </Example>
             <Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications}
                          userProfile={{email: "test@example.com"}}
                          mozLoop={mockMozLoopRooms}
                          dispatcher={dispatcher}
                          roomStore={roomStore}
                          selectedTab="rooms" />
             </Example>
--- a/browser/components/uitour/test/browser_UITour_loop.js
+++ b/browser/components/uitour/test/browser_UITour_loop.js
@@ -191,16 +191,17 @@ let tests = [
       scope: "profile"
     };
     const fxASampleProfile = {
       email: "test@example.com",
       uid: "abcd1234"
     };
     MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
     MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
+    Services.prefs.setCharPref("loop.key.fxa", "fake");
     yield MozLoopServiceInternal.notifyStatusChanged("login");
 
     // Show the Loop menu.
     yield showMenuPromise("loop");
 
     // Listen for and test the notifications that will arrive from now on.
     let tabChangePromise = new Promise(resolve => {
       gContentAPI.observe((event, params) => {
@@ -220,16 +221,17 @@ let tests = [
     });
 
     // Switch to the contacts tab.
     yield window.LoopUI.openCallPanel(null, "contacts");
 
     // Logout. The panel tab will switch back to 'rooms'.
     MozLoopServiceInternal.fxAOAuthTokenData =
       MozLoopServiceInternal.fxAOAuthProfile = null;
+    Services.prefs.clearUserPref("loop.key.fxa");
     yield MozLoopServiceInternal.notifyStatusChanged();
 
     yield tabChangePromise;
   }),
   runOffline(function test_notifyLoopChatWindowOpenedClosed(done) {
     gContentAPI.observe((event, params) => {
       is(event, "Loop:ChatWindowOpened", "Check Loop:ChatWindowOpened notification");
       gContentAPI.observe((event, params) => {
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -7,16 +7,25 @@
 ## LOCALIZATION NOTE(clientShortname2): This should not be localized and
 ## should remain "Firefox Hello" for all locales.
 clientShortname2=Firefox Hello
 clientSuperShortname=Hello
 
 rooms_tab_button_tooltip=Conversations
 contacts_tab_button_tooltip=Contacts
 
+## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two):
+## These are displayed together at the top of the panel when a user is needed to
+## sign-in again. The first "line_one" is slightly bigger font that "line_two",
+## hence the separation.
+sign_in_again_title_line_one=Please sign in again
+sign_in_again_title_line_two=to continue using Firefox Hello
+sign_in_again_button=Sign In
+sign_in_again_use_as_guest_button=Use Hello as a Guest
+
 ## LOCALIZATION_NOTE(first_time_experience.title): clientShortname will be
 ## replaced by the brand name
 first_time_experience_title={{clientShortname}} — Join the conversation
 first_time_experience_button_label=Get Started
 
 invite_header_text=Invite someone to join you.
 
 # Status text