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 225344 94dadf55808a192c4258ab6f50bc1109759e475c
parent 225343 09e683df2115fbefc11dad146601e15728c96a8d
child 225345 06ac302569af7cd3fe2137520310b5129da5bcf0
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/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
@@ -92,16 +92,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) {
@@ -414,20 +431,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 (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.
@@ -870,16 +874,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.
@@ -990,19 +998,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
@@ -254,18 +254,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")}), 
@@ -454,45 +453,81 @@ 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}), 
           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: "footer"},
+            React.DOM.div({className: "user-details"},
+              UserIdentity({displayName: displayName}),
+              AvailabilityDropdown(null)
+            ),
             AuthLink(null), 
             SettingsDropdown(null)
           )
         )
       );
     }
   });
 
@@ -577,16 +612,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
@@ -254,18 +254,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")} />
@@ -454,45 +453,81 @@ 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} />
           <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>
       );
     }
   });
 
@@ -577,16 +612,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/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -4,17 +4,18 @@
 /**
  * Test FxA logins with Loop.
  */
 
 "use strict";
 
 const {
   LOOP_SESSION_TYPE,
-  gFxAOAuthTokenData
+  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?";
 const HAWK_TOKEN_LENGTH = 64;
 
 add_task(function* setup() {
   Services.prefs.setCharPref("loop.server", BASE_URL);
   Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
@@ -174,26 +175,37 @@ add_task(function* basicAuthorizationAnd
   let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST);
   let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join("");
   ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token");
 
   // 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();
+  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");
 
   let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
   prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join("");
   ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token");
+
+  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");
 });
 
 add_task(function* loginWithParams401() {
   resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -94,30 +94,40 @@ function promiseOAuthParamsSetup(baseURL
   return deferred.promise;
 }
 
 function resetFxA() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
   global.gFxAOAuthTokenData = null;
+  global.gFxAOAuthProfile = null;
 }
 
 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
@@ -31,29 +31,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;
@@ -146,16 +149,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 which simply returns the simplePushURL with
  * padding as the hawk session token.
  */
 function registration(request, response) {
   let body = NetUtil.readInputStreamToString(request.bodyInputStream,
                                              request.bodyInputStream.available());
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -120,21 +120,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", dashed: "true", style: {width: "280px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
                 IncomingCallView({model: mockConversationModel})
               )
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -120,22 +120,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" dashed="true" style={{width: "280px"}}>
               <div className="fx-embedded">
                 <IncomingCallView model={mockConversationModel} />
               </div>
             </Example>