From 35d61aae2c48038640e79eac2877d1dd65411ae3 Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Thu, 11 Sep 2014 15:36:14 -0400 Subject: [PATCH] Bug 1047146 - Add current username to the Loop panel footer. r=mattn r=niko --HG-- extra : rebase_source : ba9f55a9a28d1a4df6284582ded6d761e269d5cf --- browser/components/loop/MozLoopAPI.jsm | 31 +++++++++++++ browser/components/loop/MozLoopService.jsm | 26 ++++++++++- browser/components/loop/content/js/panel.js | 46 +++++++++++++++++-- browser/components/loop/content/js/panel.jsx | 46 +++++++++++++++++-- .../loop/test/desktop-local/panel_test.js | 6 +-- .../loop/test/mochitest/browser_fxa_login.js | 12 +++++ .../components/loop/test/mochitest/head.js | 20 ++++++-- .../loop/test/mochitest/loop_fxa.sjs | 18 +++++++- browser/components/loop/ui/ui-showcase.js | 13 ++++++ browser/components/loop/ui/ui-showcase.jsx | 13 ++++++ 10 files changed, 211 insertions(+), 20 deletions(-) diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index f0318246888b..cdd18bad8eed 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -108,6 +108,23 @@ function injectLoopAPI(targetWindow) { 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. */ @@ -440,10 +457,24 @@ function injectLoopAPI(targetWindow) { }, }; + 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 diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index 1936eaa57a17..5d4ba2e1d7e8 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -46,6 +46,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", 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"); @@ -76,6 +79,7 @@ let gInitializeTimer = null; let gFxAOAuthClientPromise = null; let gFxAOAuthClient = null; let gFxAOAuthTokenData = null; +let gFxAOAuthProfile = null; let gErrors = new Map(); /** @@ -884,6 +888,10 @@ this.MozLoopService = { MozLoopServiceInternal.doNotDisturb = aFlag; }, + get userProfile() { + return gFxAOAuthProfile; + }, + get errors() { return MozLoopServiceInternal.errors; }, @@ -1004,9 +1012,23 @@ this.MozLoopService = { } 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; }); }, diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index df1e4cb23977..d7d61f7eb7b1 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -229,8 +229,7 @@ loop.panel = (function(_, mozL10n) { }, _isSignedIn: function() { - // XXX to be implemented - bug 979845 - return !!navigator.mozLoop.loggedInToFxA; + return !!navigator.mozLoop.userProfile; }, render: function() { @@ -448,6 +447,19 @@ loop.panel = (function(_, mozL10n) { } }); + /** + * 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. */ @@ -456,12 +468,32 @@ loop.panel = (function(_, mozL10n) { 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, @@ -478,7 +510,10 @@ loop.panel = (function(_, mozL10n) { ) ), React.DOM.div({className: "footer"}, - AvailabilityDropdown(null), + React.DOM.div({className: "user-details"}, + UserIdentity({displayName: displayName}), + AvailabilityDropdown(null) + ), AuthLink(null), SettingsDropdown(null) ) @@ -543,6 +578,7 @@ loop.panel = (function(_, mozL10n) { return { init: init, + UserIdentity: UserIdentity, AvailabilityDropdown: AvailabilityDropdown, CallUrlResult: CallUrlResult, PanelView: PanelView, diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index a4243779233b..8b012deb8813 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -229,8 +229,7 @@ loop.panel = (function(_, mozL10n) { }, _isSignedIn: function() { - // XXX to be implemented - bug 979845 - return !!navigator.mozLoop.loggedInToFxA; + return !!navigator.mozLoop.userProfile; }, render: function() { @@ -448,6 +447,19 @@ loop.panel = (function(_, mozL10n) { } }); + /** + * FxA user identity (guest/authenticated) component. + */ + var UserIdentity = React.createClass({ + render: function() { + return ( +

+ {this.props.displayName} +

+ ); + } + }); + /** * Panel view. */ @@ -456,12 +468,32 @@ loop.panel = (function(_, mozL10n) { 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 (
- +
+ + +
@@ -543,6 +578,7 @@ loop.panel = (function(_, mozL10n) { return { init: init, + UserIdentity: UserIdentity, AvailabilityDropdown: AvailabilityDropdown, CallUrlResult: CallUrlResult, PanelView: PanelView, diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index 32a078513eed..a62eb7f89036 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -194,7 +194,7 @@ describe("loop.panel", function() { }); 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()); @@ -205,7 +205,7 @@ describe("loop.panel", function() { }); 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()); @@ -234,7 +234,7 @@ describe("loop.panel", function() { }); 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( diff --git a/browser/components/loop/test/mochitest/browser_fxa_login.js b/browser/components/loop/test/mochitest/browser_fxa_login.js index 211ef535654d..663b01781a33 100644 --- a/browser/components/loop/test/mochitest/browser_fxa_login.js +++ b/browser/components/loop/test/mochitest/browser_fxa_login.js @@ -9,6 +9,7 @@ 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?"; @@ -224,11 +225,22 @@ add_task(function* basicAuthorizationAndRegistration() { // 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"); }); diff --git a/browser/components/loop/test/mochitest/head.js b/browser/components/loop/test/mochitest/head.js index 5b191d345e91..de0be8cc1c22 100644 --- a/browser/components/loop/test/mochitest/head.js +++ b/browser/components/loop/test/mochitest/head.js @@ -58,17 +58,19 @@ function promiseGetMozLoopAPI() { * * 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"); @@ -103,6 +105,7 @@ function resetFxA() { global.gFxAOAuthClientPromise = null; global.gFxAOAuthClient = null; global.gFxAOAuthTokenData = null; + global.gFxAOAuthProfile = null; const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA); Services.prefs.clearUserPref(fxASessionPref); } @@ -119,6 +122,15 @@ function promiseDeletedOAuthParams(baseURL) { 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. */ diff --git a/browser/components/loop/test/mochitest/loop_fxa.sjs b/browser/components/loop/test/mochitest/loop_fxa.sjs index e16666d02679..c6c80704f644 100644 --- a/browser/components/loop/test/mochitest/loop_fxa.sjs +++ b/browser/components/loop/test/mochitest/loop_fxa.sjs @@ -37,6 +37,9 @@ function handleRequest(request, response) { case "/get_registration": // Test-only get_registration(request, response); return; + case "/profile/profile": + profile(request, response); + return; } response.setStatusLine(request.httpVersion, 404, "Not Found"); } @@ -49,7 +52,7 @@ function handleRequest(request, 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); @@ -166,6 +169,19 @@ function token(request, response) { 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 * diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index 75a8a17b09fa..4a3fd99dccbe 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -126,11 +126,24 @@ 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"}}) ) ), diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index e74f387ab7ec..715f45feb759 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -126,12 +126,25 @@ + + + + + + + + +