Bug 1047146 - Add current username to the Loop panel footer. r=mattn r=niko

--HG--
extra : rebase_source : ba9f55a9a28d1a4df6284582ded6d761e269d5cf
This commit is contained in:
Jared Wein 2014-09-11 15:36:14 -04:00
Родитель 9d28679208
Коммит 35d61aae2c
10 изменённых файлов: 211 добавлений и 20 удалений

Просмотреть файл

@ -108,6 +108,23 @@ function injectLoopAPI(targetWindow) {
let contactsAPI; let contactsAPI;
let api = { 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. * 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); let contentObj = Cu.createObjectIn(targetWindow);
Object.defineProperties(contentObj, api); Object.defineProperties(contentObj, api);
Object.seal(contentObj); Object.seal(contentObj);
Cu.makeObjectPropsNormal(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() { targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
// We do this in a getter, so that we create these objects // We do this in a getter, so that we create these objects

Просмотреть файл

@ -46,6 +46,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
"resource://services-crypto/utils.js"); "resource://services-crypto/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
"resource://gre/modules/FxAccountsProfileClient.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HawkClient", XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
"resource://services-common/hawkclient.js"); "resource://services-common/hawkclient.js");
@ -76,6 +79,7 @@ let gInitializeTimer = null;
let gFxAOAuthClientPromise = null; let gFxAOAuthClientPromise = null;
let gFxAOAuthClient = null; let gFxAOAuthClient = null;
let gFxAOAuthTokenData = null; let gFxAOAuthTokenData = null;
let gFxAOAuthProfile = null;
let gErrors = new Map(); let gErrors = new Map();
/** /**
@ -884,6 +888,10 @@ this.MozLoopService = {
MozLoopServiceInternal.doNotDisturb = aFlag; MozLoopServiceInternal.doNotDisturb = aFlag;
}, },
get userProfile() {
return gFxAOAuthProfile;
},
get errors() { get errors() {
return MozLoopServiceInternal.errors; return MozLoopServiceInternal.errors;
}, },
@ -1004,9 +1012,23 @@ this.MozLoopService = {
} }
return gFxAOAuthTokenData; return gFxAOAuthTokenData;
})); }));
}, }).then(tokenData => {
error => { 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; gFxAOAuthTokenData = null;
gFxAOAuthProfile = null;
throw error; throw error;
}); });
}, },

Просмотреть файл

@ -229,8 +229,7 @@ loop.panel = (function(_, mozL10n) {
}, },
_isSignedIn: function() { _isSignedIn: function() {
// XXX to be implemented - bug 979845 return !!navigator.mozLoop.userProfile;
return !!navigator.mozLoop.loggedInToFxA;
}, },
render: function() { 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. * Panel view.
*/ */
@ -456,12 +468,32 @@ loop.panel = (function(_, mozL10n) {
notifications: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired, client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests // 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() { render: function() {
var NotificationListView = sharedViews.NotificationListView; var NotificationListView = sharedViews.NotificationListView;
var displayName = this.state.userProfile && this.state.userProfile.email ||
__("display_name_guest");
return ( return (
React.DOM.div(null, React.DOM.div(null,
NotificationListView({notifications: this.props.notifications, NotificationListView({notifications: this.props.notifications,
@ -478,7 +510,10 @@ loop.panel = (function(_, mozL10n) {
) )
), ),
React.DOM.div({className: "footer"}, React.DOM.div({className: "footer"},
AvailabilityDropdown(null), React.DOM.div({className: "user-details"},
UserIdentity({displayName: displayName}),
AvailabilityDropdown(null)
),
AuthLink(null), AuthLink(null),
SettingsDropdown(null) SettingsDropdown(null)
) )
@ -543,6 +578,7 @@ loop.panel = (function(_, mozL10n) {
return { return {
init: init, init: init,
UserIdentity: UserIdentity,
AvailabilityDropdown: AvailabilityDropdown, AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult, CallUrlResult: CallUrlResult,
PanelView: PanelView, PanelView: PanelView,

Просмотреть файл

@ -229,8 +229,7 @@ loop.panel = (function(_, mozL10n) {
}, },
_isSignedIn: function() { _isSignedIn: function() {
// XXX to be implemented - bug 979845 return !!navigator.mozLoop.userProfile;
return !!navigator.mozLoop.loggedInToFxA;
}, },
render: function() { render: function() {
@ -448,6 +447,19 @@ loop.panel = (function(_, mozL10n) {
} }
}); });
/**
* FxA user identity (guest/authenticated) component.
*/
var UserIdentity = React.createClass({
render: function() {
return (
<p className="user-identity">
{this.props.displayName}
</p>
);
}
});
/** /**
* Panel view. * Panel view.
*/ */
@ -456,12 +468,32 @@ loop.panel = (function(_, mozL10n) {
notifications: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired, client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests // 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() { render: function() {
var NotificationListView = sharedViews.NotificationListView; var NotificationListView = sharedViews.NotificationListView;
var displayName = this.state.userProfile && this.state.userProfile.email ||
__("display_name_guest");
return ( return (
<div> <div>
<NotificationListView notifications={this.props.notifications} <NotificationListView notifications={this.props.notifications}
@ -478,7 +510,10 @@ loop.panel = (function(_, mozL10n) {
</Tab> </Tab>
</TabView> </TabView>
<div className="footer"> <div className="footer">
<AvailabilityDropdown /> <div className="user-details">
<UserIdentity displayName={displayName} />
<AvailabilityDropdown />
</div>
<AuthLink /> <AuthLink />
<SettingsDropdown /> <SettingsDropdown />
</div> </div>
@ -543,6 +578,7 @@ loop.panel = (function(_, mozL10n) {
return { return {
init: init, init: init,
UserIdentity: UserIdentity,
AvailabilityDropdown: AvailabilityDropdown, AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult, CallUrlResult: CallUrlResult,
PanelView: PanelView, PanelView: PanelView,

Просмотреть файл

@ -194,7 +194,7 @@ describe("loop.panel", function() {
}); });
it("should show a signout entry when user is authenticated", 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()); 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() { 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()); 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() { 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()); var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
TestUtils.Simulate.click( TestUtils.Simulate.click(

Просмотреть файл

@ -9,6 +9,7 @@
const { const {
gFxAOAuthTokenData, gFxAOAuthTokenData,
gFxAOAuthProfile,
} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?"; 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. // to be able to check for success on the second registration.
mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa"; 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(); let tokenData = yield MozLoopService.logInToFxA();
yield promiseObserverNotified("loop-status-changed");
ise(tokenData.access_token, "code1_access_token", "Check access_token"); ise(tokenData.access_token, "code1_access_token", "Check access_token");
ise(tokenData.scope, "profile", "Check scope"); ise(tokenData.scope, "profile", "Check scope");
ise(tokenData.token_type, "bearer", "Check token_type"); 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); let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL"); ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
}); });

Просмотреть файл

@ -58,17 +58,19 @@ function promiseGetMozLoopAPI() {
* *
* This assumes that the tests are running in a generatorTest. * 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. // Set prefs to ensure we don't access the network externally.
Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/"); Services.prefs.setCharPref("services.push.serverURL", aOverrideOptions.pushURL || "ws://localhost/");
Services.prefs.setCharPref("loop.server", "http://localhost/"); Services.prefs.setCharPref("loop.server", aOverrideOptions.loopURL || "http://localhost/");
// Turn off the network for loop tests, so that we don't // Turn off the network for loop tests, so that we don't
// try to access the remote servers. If we want to turn this // try to access the remote servers. If we want to turn this
// back on in future, be careful to check for intermittent // back on in future, be careful to check for intermittent
// failures. // failures.
let wasOffline = Services.io.offline; let wasOffline = Services.io.offline;
Services.io.offline = true; if (!aOverrideOptions.stayOnline) {
Services.io.offline = true;
}
registerCleanupFunction(function() { registerCleanupFunction(function() {
Services.prefs.clearUserPref("services.push.serverURL"); Services.prefs.clearUserPref("services.push.serverURL");
@ -103,6 +105,7 @@ function resetFxA() {
global.gFxAOAuthClientPromise = null; global.gFxAOAuthClientPromise = null;
global.gFxAOAuthClient = null; global.gFxAOAuthClient = null;
global.gFxAOAuthTokenData = null; global.gFxAOAuthTokenData = null;
global.gFxAOAuthProfile = null;
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA); const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
Services.prefs.clearUserPref(fxASessionPref); Services.prefs.clearUserPref(fxASessionPref);
} }
@ -119,6 +122,15 @@ function promiseDeletedOAuthParams(baseURL) {
return deferred.promise; 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. * Get the last registration on the test server.
*/ */

Просмотреть файл

@ -37,6 +37,9 @@ function handleRequest(request, response) {
case "/get_registration": // Test-only case "/get_registration": // Test-only
get_registration(request, response); get_registration(request, response);
return; return;
case "/profile/profile":
profile(request, response);
return;
} }
response.setStatusLine(request.httpVersion, 404, "Not Found"); 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. * 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 * 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) { function setup_params(request, response) {
response.setHeader("Content-Type", "text/plain", false); response.setHeader("Content-Type", "text/plain", false);
@ -166,6 +169,19 @@ function token(request, response) {
response.write(JSON.stringify(tokenData, null, 2)); 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 * POST /registration
* *

Просмотреть файл

@ -126,11 +126,24 @@
PanelView({client: mockClient, notifications: notifications, PanelView({client: mockClient, notifications: notifications,
callUrl: "http://invalid.example.url/"}) 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"}}, Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications}) 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"}}, Example({summary: "Error Notification", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: errNotifications}) 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"}})
) )
), ),

Просмотреть файл

@ -126,12 +126,25 @@
<PanelView client={mockClient} notifications={notifications} <PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/" /> callUrl="http://invalid.example.url/" />
</Example> </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"}}> <Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications} /> <PanelView client={mockClient} notifications={notifications} />
</Example> </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"}}> <Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={errNotifications}/> <PanelView client={mockClient} notifications={errNotifications}/>
</Example> </Example>
<Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={errNotifications}
userProfile={{email: "test@example.com"}} />
</Example>
</Section> </Section>
<Section name="IncomingCallView"> <Section name="IncomingCallView">