Bug 1047146 - Add current username to the Loop panel footer. r=mattn r=niko
authorJared Wein <jwein@mozilla.com>
Thu, 11 Sep 2014 15:36:14 -0400
changeset 225363 2d94ee52c6d810b7845dc966dc910d01cdea37e2
parent 225362 19f2399950b5e310f78cfd501cce4002ad3d61e2
child 225364 076a776862b486f257bebce524fc95aedb4a5b2a
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmattn, niko
bugs1047146
milestone34.0a2
Bug 1047146 - Add current username to the Loop panel footer. r=mattn r=niko
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/test/desktop-local/panel_test.js
browser/components/loop/test/mochitest/browser_fxa_login.js
browser/components/loop/test/mochitest/head.js
browser/components/loop/test/mochitest/loop_fxa.sjs
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -104,16 +104,33 @@ const injectObjectAPI = function(api, ta
 function injectLoopAPI(targetWindow) {
   let ringer;
   let ringerStopper;
   let appVersionInfo;
   let contactsAPI;
 
   let api = {
     /**
+     * Gets an object with data that represents the currently
+     * authenticated user's identity.
+     */
+    userProfile: {
+      enumerable: true,
+      get: function() {
+        if (!MozLoopService.userProfile)
+          return null;
+        let userProfile = Cu.cloneInto({
+          email: MozLoopService.userProfile.email,
+          uid: MozLoopService.userProfile.uid
+        }, targetWindow);
+        return userProfile;
+      }
+    },
+
+    /**
      * Sets and gets the "do not disturb" mode activation flag.
      */
     doNotDisturb: {
       enumerable: true,
       get: function() {
         return MozLoopService.doNotDisturb;
       },
       set: function(aFlag) {
@@ -435,20 +452,34 @@ function injectLoopAPI(targetWindow) {
             OS: appInfo.OS
           }, targetWindow);
         }
         return appVersionInfo;
       }
     },
   };
 
+  function onStatusChanged(aSubject, aTopic, aData) {
+    let event = new targetWindow.CustomEvent("LoopStatusChanged");
+    targetWindow.dispatchEvent(event)
+  };
+
+  function onDOMWindowDestroyed(aSubject, aTopic, aData) {
+    if (targetWindow && aSubject != targetWindow)
+      return;
+    Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed");
+    Services.obs.removeObserver(onStatusChanged, "loop-status-changed");
+  };
+
   let contentObj = Cu.createObjectIn(targetWindow);
   Object.defineProperties(contentObj, api);
   Object.seal(contentObj);
   Cu.makeObjectPropsNormal(contentObj);
+  Services.obs.addObserver(onStatusChanged, "loop-status-changed", false);
+  Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
 
   targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
     // We do this in a getter, so that we create these objects
     // only on demand (this is a potential concern, since
     // otherwise we might add one per iframe, and keep them
     // alive for as long as the window is alive).
     delete targetWindow.navigator.wrappedJSObject.mozLoop;
     return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -41,16 +41,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
                                   "resource://services-common/utils.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
                                   "resource://services-crypto/utils.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
+                                  "resource://gre/modules/FxAccountsProfileClient.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
                                   "resource://services-common/hawkclient.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
                                   "resource://services-common/hawkrequest.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
@@ -71,16 +74,17 @@ let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
 let gRegisteredLoopServer = false;
 let gLocalizedStrings =  null;
 let gInitializeTimer = null;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
 let gFxAOAuthTokenData = null;
+let gFxAOAuthProfile = null;
 let gErrors = new Map();
 
 /**
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
@@ -879,16 +883,20 @@ this.MozLoopService = {
    * Sets MozLoopService "do not disturb" value.
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     MozLoopServiceInternal.doNotDisturb = aFlag;
   },
 
+  get userProfile() {
+    return gFxAOAuthProfile;
+  },
+
   get errors() {
     return MozLoopServiceInternal.errors;
   },
 
   /**
    * Returns the current locale
    *
    * @return {String} The code of the current locale.
@@ -999,19 +1007,33 @@ this.MozLoopService = {
       return gRegisteredDeferred.promise.then(Task.async(function*() {
         if (gPushHandler.pushUrl) {
           yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
         } else {
           throw new Error("No pushUrl for FxA registration");
         }
         return gFxAOAuthTokenData;
       }));
-    },
-    error => {
+    }).then(tokenData => {
+      let client = new FxAccountsProfileClient({
+        serverURL: gFxAOAuthClient.parameters.profile_uri,
+        token: tokenData.access_token
+      });
+      client.fetchProfile().then(result => {
+        gFxAOAuthProfile = result;
+        MozLoopServiceInternal.notifyStatusChanged();
+      }, error => {
+        console.error("Failed to retrieve profile", error);
+        gFxAOAuthProfile = null;
+        MozLoopServiceInternal.notifyStatusChanged();
+      });
+      return tokenData;
+    }).catch(error => {
       gFxAOAuthTokenData = null;
+      gFxAOAuthProfile = null;
       throw error;
     });
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -224,18 +224,17 @@ loop.panel = (function(_, mozL10n) {
         // XXX to be implemented - bug 979845
         navigator.mozLoop.logOutFromFxA();
       } else {
         navigator.mozLoop.logInToFxA();
       }
     },
 
     _isSignedIn: function() {
-      // XXX to be implemented - bug 979845
-      return !!navigator.mozLoop.loggedInToFxA;
+      return !!navigator.mozLoop.userProfile;
     },
 
     render: function() {
       var cx = React.addons.classSet;
       return (
         React.DOM.div({className: "settings-menu dropdown"}, 
           React.DOM.a({className: "btn btn-settings", onClick: this.showDropdownMenu, 
              title: __("settings_menu_button_tooltip")}), 
@@ -444,46 +443,82 @@ loop.panel = (function(_, mozL10n) {
             __("panel_footer_signin_or_signup_link")
           )
         )
       );
     }
   });
 
   /**
+   * FxA user identity (guest/authenticated) component.
+   */
+  var UserIdentity = React.createClass({displayName: 'UserIdentity',
+    render: function() {
+      return (
+        React.DOM.p({className: "user-identity"},
+          this.props.displayName
+        )
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
-      callUrl: React.PropTypes.string
+      callUrl: React.PropTypes.string,
+      userProfile: React.PropTypes.object,
+    },
+
+    getInitialState: function() {
+      return {
+        userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
+      };
+    },
+
+    _onAuthStatusChange: function() {
+      this.setState({userProfile: navigator.mozLoop.userProfile});
+    },
+
+    componentDidMount: function() {
+      window.addEventListener("LoopStatusChanged", this._onAuthStatusChange);
+    },
+
+    componentWillUnmount: function() {
+      window.removeEventListener("LoopStatusChanged", this._onAuthStatusChange);
     },
 
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
-
+      var displayName = this.state.userProfile && this.state.userProfile.email ||
+                        __("display_name_guest");
       return (
         React.DOM.div(null, 
           NotificationListView({notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
           TabView({onSelect: this.selectTab}, 
             Tab({name: "call"}, 
               CallUrlResult({client: this.props.client, 
                              notifications: this.props.notifications, 
                              callUrl: this.props.callUrl}), 
               ToSView(null)
             ), 
             Tab({name: "contacts"}, 
               React.DOM.span(null, "contacts")
             )
           ), 
           React.DOM.div({className: "footer"}, 
-            AvailabilityDropdown(null), 
+            React.DOM.div({className: "user-details"},
+              UserIdentity({displayName: displayName}),
+              AvailabilityDropdown(null)
+            ),
             AuthLink(null), 
             SettingsDropdown(null)
           )
         )
       );
     }
   });
 
@@ -538,16 +573,17 @@ 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,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -224,18 +224,17 @@ loop.panel = (function(_, mozL10n) {
         // XXX to be implemented - bug 979845
         navigator.mozLoop.logOutFromFxA();
       } else {
         navigator.mozLoop.logInToFxA();
       }
     },
 
     _isSignedIn: function() {
-      // XXX to be implemented - bug 979845
-      return !!navigator.mozLoop.loggedInToFxA;
+      return !!navigator.mozLoop.userProfile;
     },
 
     render: function() {
       var cx = React.addons.classSet;
       return (
         <div className="settings-menu dropdown">
           <a className="btn btn-settings" onClick={this.showDropdownMenu}
              title={__("settings_menu_button_tooltip")} />
@@ -444,46 +443,82 @@ loop.panel = (function(_, mozL10n) {
             {__("panel_footer_signin_or_signup_link")}
           </a>
         </p>
       );
     }
   });
 
   /**
+   * FxA user identity (guest/authenticated) component.
+   */
+  var UserIdentity = React.createClass({
+    render: function() {
+      return (
+        <p className="user-identity">
+          {this.props.displayName}
+        </p>
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
-      callUrl: React.PropTypes.string
+      callUrl: React.PropTypes.string,
+      userProfile: React.PropTypes.object,
+    },
+
+    getInitialState: function() {
+      return {
+        userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
+      };
+    },
+
+    _onAuthStatusChange: function() {
+      this.setState({userProfile: navigator.mozLoop.userProfile});
+    },
+
+    componentDidMount: function() {
+      window.addEventListener("LoopStatusChanged", this._onAuthStatusChange);
+    },
+
+    componentWillUnmount: function() {
+      window.removeEventListener("LoopStatusChanged", this._onAuthStatusChange);
     },
 
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
-
+      var displayName = this.state.userProfile && this.state.userProfile.email ||
+                        __("display_name_guest");
       return (
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
           <TabView onSelect={this.selectTab}>
             <Tab name="call">
               <CallUrlResult client={this.props.client}
                              notifications={this.props.notifications}
                              callUrl={this.props.callUrl} />
               <ToSView />
             </Tab>
             <Tab name="contacts">
               <span>contacts</span>
             </Tab>
           </TabView>
           <div className="footer">
-            <AvailabilityDropdown />
+            <div className="user-details">
+              <UserIdentity displayName={displayName} />
+              <AvailabilityDropdown />
+            </div>
             <AuthLink />
             <SettingsDropdown />
           </div>
         </div>
       );
     }
   });
 
@@ -538,16 +573,17 @@ 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,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -189,28 +189,28 @@ describe("loop.panel", function() {
 
           expect(view.getDOMNode().querySelectorAll(".icon-signout"))
             .to.have.length.of(0);
           expect(view.getDOMNode().querySelectorAll(".icon-signin"))
             .to.have.length.of(1);
         });
 
       it("should show a signout entry when user is authenticated", function() {
-        navigator.mozLoop.loggedInToFxA = true;
+        navigator.mozLoop.userProfile = {email: "test@example.com"};
 
         var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
 
         expect(view.getDOMNode().querySelectorAll(".icon-signout"))
           .to.have.length.of(1);
         expect(view.getDOMNode().querySelectorAll(".icon-signin"))
           .to.have.length.of(0);
       });
 
       it("should show an account entry when user is authenticated", function() {
-        navigator.mozLoop.loggedInToFxA = true;
+        navigator.mozLoop.userProfile = {email: "test@example.com"};
 
         var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
 
         expect(view.getDOMNode().querySelectorAll(".icon-account"))
           .to.have.length.of(1);
       });
 
       it("should hide any account entry when user is not authenticated",
@@ -229,17 +229,17 @@ describe("loop.panel", function() {
 
         TestUtils.Simulate.click(
           view.getDOMNode().querySelector(".icon-signin"));
 
         sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
       });
 
       it("should sign out the user on click when authenticated", function() {
-        navigator.mozLoop.loggedInToFxA = true;
+        navigator.mozLoop.userProfile = {email: "test@example.com"};
         var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
 
         TestUtils.Simulate.click(
           view.getDOMNode().querySelector(".icon-signout"));
 
         sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
       });
     });
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -4,16 +4,17 @@
 /**
  * Test FxA logins with Loop.
  */
 
 "use strict";
 
 const {
   gFxAOAuthTokenData,
+  gFxAOAuthProfile,
 } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 
 const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
 
 add_task(function* setup() {
   Services.prefs.setCharPref("loop.server", BASE_URL);
   Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
   registerCleanupFunction(function* () {
@@ -219,21 +220,32 @@ add_task(function* basicAuthorizationAnd
   info("registering");
   mockPushHandler.pushUrl = "https://localhost/pushUrl/guest";
   yield MozLoopService.register(mockPushHandler);
 
   // Normally the same pushUrl would be registered but we change it in the test
   // to be able to check for success on the second registration.
   mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa";
 
+  yield loadLoopPanel({loopURL: BASE_URL, stayOnline: true});
+  let loopDoc = document.getElementById("loop").contentDocument;
+  let visibleEmail = loopDoc.getElementsByClassName("user-identity")[0];
+  is(visibleEmail.textContent, "Guest", "Guest should be displayed on the panel when not logged in");
+  is(MozLoopService.userProfile, null, "profile should be null before log-in");
+
   let tokenData = yield MozLoopService.logInToFxA();
+  yield promiseObserverNotified("loop-status-changed");
   ise(tokenData.access_token, "code1_access_token", "Check access_token");
   ise(tokenData.scope, "profile", "Check scope");
   ise(tokenData.token_type, "bearer", "Check token_type");
 
+  is(MozLoopService.userProfile.email, "test@example.com", "email should exist in the profile data");
+  is(MozLoopService.userProfile.uid, "1234abcd", "uid should exist in the profile data");
+  is(visibleEmail.textContent, "test@example.com", "the email should be correct on the panel");
+
   let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
 });
 
 add_task(function* loginWithParams401() {
   resetFxA();
   let params = {
     client_id: "client_id",
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -53,27 +53,29 @@ function promiseGetMozLoopAPI() {
 }
 
 /**
  * Loads the loop panel by clicking the button and waits for its open to complete.
  * It also registers
  *
  * This assumes that the tests are running in a generatorTest.
  */
-function loadLoopPanel() {
+function loadLoopPanel(aOverrideOptions = {}) {
   // Set prefs to ensure we don't access the network externally.
-  Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
-  Services.prefs.setCharPref("loop.server", "http://localhost/");
+  Services.prefs.setCharPref("services.push.serverURL", aOverrideOptions.pushURL || "ws://localhost/");
+  Services.prefs.setCharPref("loop.server", aOverrideOptions.loopURL || "http://localhost/");
 
   // Turn off the network for loop tests, so that we don't
   // try to access the remote servers. If we want to turn this
   // back on in future, be careful to check for intermittent
   // failures.
   let wasOffline = Services.io.offline;
-  Services.io.offline = true;
+  if (!aOverrideOptions.stayOnline) {
+    Services.io.offline = true;
+  }
 
   registerCleanupFunction(function() {
     Services.prefs.clearUserPref("services.push.serverURL");
     Services.prefs.clearUserPref("loop.server");
     Services.io.offline = wasOffline;
   });
 
   // Turn off animations to make tests quicker.
@@ -98,32 +100,42 @@ function promiseOAuthParamsSetup(baseURL
 }
 
 function resetFxA() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   global.gHawkClient = null;
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
   global.gFxAOAuthTokenData = null;
+  global.gFxAOAuthProfile = null;
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.clearUserPref(fxASessionPref);
 }
 
 function promiseDeletedOAuthParams(baseURL) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
               createInstance(Ci.nsIXMLHttpRequest);
   xhr.open("DELETE", baseURL + "/setup_params", true);
   xhr.addEventListener("load", () => deferred.resolve(xhr));
   xhr.addEventListener("error", deferred.reject);
   xhr.send();
 
   return deferred.promise;
 }
 
+function promiseObserverNotified(aTopic) {
+  let deferred = Promise.defer();
+  Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
+    Services.obs.removeObserver(onNotification, aTopic);
+      deferred.resolve({subject: aSubject, data: aData});
+    }, aTopic, false);
+  return deferred.promise;
+}
+
 /**
  * Get the last registration on the test server.
  */
 function promiseOAuthGetRegistration(baseURL) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
               createInstance(Ci.nsIXMLHttpRequest);
   xhr.open("GET", baseURL + "/get_registration", true);
--- a/browser/components/loop/test/mochitest/loop_fxa.sjs
+++ b/browser/components/loop/test/mochitest/loop_fxa.sjs
@@ -32,29 +32,32 @@ function handleRequest(request, response
       token(request, response);
       return;
     case "/registration":
       registration(request, response);
       return;
     case "/get_registration": // Test-only
       get_registration(request, response);
       return;
+    case "/profile/profile":
+      profile(request, response);
+      return;
   }
   response.setStatusLine(request.httpVersion, 404, "Not Found");
 }
 
 /**
  * POST /setup_params
  * DELETE /setup_params
  *
  * Test-only endpoint to setup the /fxa-oauth/params response.
  *
  * For a POST the X-Params header should contain a JSON object with keys to set for /fxa-oauth/params.
  * A DELETE request will delete the stored parameters and should be run in a cleanup function to
- * avoid interfering with subsequen tests.
+ * avoid interfering with subsequent tests.
  */
 function setup_params(request, response) {
   response.setHeader("Content-Type", "text/plain", false);
   if (request.method == "DELETE") {
     setSharedState("/fxa-oauth/params", "");
     setSharedState("/registration", "");
     response.write("Params deleted");
     return;
@@ -162,16 +165,29 @@ function token(request, response) {
     scope: "profile",
     token_type: "bearer",
   };
   response.setHeader("Content-Type", "application/json; charset=utf-8", false);
   response.write(JSON.stringify(tokenData, null, 2));
 }
 
 /**
+ * GET /profile
+ *
+ */
+function profile(request, response) {
+  response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+  let profile = {
+    email: "test@example.com",
+    uid: "1234abcd",
+  };
+  response.write(JSON.stringify(profile, null, 2));
+}
+
+/**
  * POST /registration
  *
  * Mock Loop registration endpoint. Hawk Authorization headers are expected only for FxA sessions.
  */
 function registration(request, response) {
   let body = NetUtil.readInputStreamToString(request.bodyInputStream,
                                              request.bodyInputStream.available());
   let payload = JSON.parse(body);
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -121,21 +121,34 @@
           Section({name: "PanelView"}, 
             React.DOM.p({className: "note"}, 
               React.DOM.strong(null, "Note:"), " 332px wide."
             ), 
             Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: notifications, 
                          callUrl: "http://invalid.example.url/"})
             ), 
+            Example({summary: "Call URL retrieved - authenticated", dashed: "true", style: {width: "332px"}}, 
+              PanelView({client: mockClient, notifications: notifications, 
+                         callUrl: "http://invalid.example.url/",
+                         userProfile: {email: "test@example.com"}})
+            ), 
             Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: notifications})
             ), 
+            Example({summary: "Pending call url retrieval - authenticated", dashed: "true", style: {width: "332px"}}, 
+              PanelView({client: mockClient, notifications: notifications,
+                         userProfile: {email: "test@example.com"}})
+            ), 
             Example({summary: "Error Notification", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: errNotifications})
+            ),
+            Example({summary: "Error Notification - authenticated", dashed: "true", style: {width: "332px"}}, 
+              PanelView({client: mockClient, notifications: errNotifications,
+                         userProfile: {email: "test@example.com"}})
             )
           ), 
 
           Section({name: "IncomingCallView"}, 
             Example({summary: "Default / incoming video call", dashed: "true", style: {width: "280px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
                 IncomingCallView({model: mockConversationModel, 
                                   video: {enabled: true}})
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -121,22 +121,35 @@
           <Section name="PanelView">
             <p className="note">
               <strong>Note:</strong> 332px wide.
             </p>
             <Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications}
                          callUrl="http://invalid.example.url/" />
             </Example>
+            <Example summary="Call URL retrieved - authenticated" dashed="true" style={{width: "332px"}}>
+              <PanelView client={mockClient} notifications={notifications}
+                         callUrl="http://invalid.example.url/"
+                         userProfile={{email: "test@example.com"}} />
+            </Example>
             <Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications} />
             </Example>
+            <Example summary="Pending call url retrieval - authenticated" dashed="true" style={{width: "332px"}}>
+              <PanelView client={mockClient} notifications={notifications}
+                         userProfile={{email: "test@example.com"}} />
+            </Example>
             <Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={errNotifications}/>
             </Example>
+            <Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}>
+              <PanelView client={mockClient} notifications={errNotifications}
+                         userProfile={{email: "test@example.com"}} />
+            </Example>
           </Section>
 
           <Section name="IncomingCallView">
             <Example summary="Default / incoming video call" dashed="true" style={{width: "280px"}}>
               <div className="fx-embedded">
                 <IncomingCallView model={mockConversationModel}
                                   video={{enabled: true}} />
               </div>