Bug 1153788 - Part 2. Ask the user to re-sign in to Loop if they don't have encryption keys for FxA. r=mikedeboer

This commit is contained in:
Mark Banner 2015-05-08 13:46:52 +01:00
Родитель d8578f5f36
Коммит a31132716f
12 изменённых файлов: 284 добавлений и 16 удалений

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

@ -23,6 +23,41 @@ body {
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 {

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

@ -214,6 +214,45 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* 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],
@ -758,6 +797,7 @@ loop.panel = (function(_, mozL10n) {
getInitialState: function() {
return {
hasEncryptionKey: this.props.mozLoop.hasEncryptionKey,
userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
};
@ -796,7 +836,10 @@ loop.panel = (function(_, mozL10n) {
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});
@ -827,7 +870,11 @@ loop.panel = (function(_, mozL10n) {
},
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() {
@ -865,6 +912,10 @@ loop.panel = (function(_, mozL10n) {
);
}
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) {
@ -937,8 +988,7 @@ loop.panel = (function(_, mozL10n) {
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());
@ -959,6 +1009,7 @@ loop.panel = (function(_, mozL10n) {
RoomEntry: RoomEntry,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
SignInRequestView: SignInRequestView,
ToSView: ToSView,
UserIdentity: UserIdentity
};

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

@ -214,6 +214,45 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* 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],
@ -758,6 +797,7 @@ loop.panel = (function(_, mozL10n) {
getInitialState: function() {
return {
hasEncryptionKey: this.props.mozLoop.hasEncryptionKey,
userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
};
@ -796,7 +836,10 @@ loop.panel = (function(_, mozL10n) {
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});
@ -827,7 +870,11 @@ loop.panel = (function(_, mozL10n) {
},
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() {
@ -865,6 +912,10 @@ loop.panel = (function(_, mozL10n) {
);
}
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) {
@ -937,8 +988,7 @@ loop.panel = (function(_, mozL10n) {
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());
@ -959,6 +1009,7 @@ loop.panel = (function(_, mozL10n) {
RoomEntry: RoomEntry,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
SignInRequestView: SignInRequestView,
ToSView: ToSView,
UserIdentity: UserIdentity
};

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

@ -669,11 +669,20 @@ function injectLoopAPI(targetWindow) {
},
},
/**
* 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);
}
},
@ -693,6 +702,18 @@ function injectLoopAPI(targetWindow) {
},
},
/**
* 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.
*

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

@ -948,9 +948,10 @@ let MozLoopServiceInternal = {
/**
* 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) {
@ -961,6 +962,10 @@ let MozLoopServiceInternal = {
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({
@ -984,11 +989,12 @@ let MozLoopServiceInternal = {
/**
* 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);
@ -1366,6 +1372,18 @@ this.MozLoopService = {
});
},
/**
* 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;
},
@ -1468,14 +1486,15 @@ this.MozLoopService = {
*
* 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;

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

@ -70,6 +70,9 @@ describe("loop.panel", function() {
on: sandbox.stub()
},
confirm: sandbox.stub(),
hasEncryptionKey: true,
logInToFxA: sandbox.stub(),
logOutFromFxA: sandbox.stub(),
notifyUITour: sandbox.stub(),
openURL: sandbox.stub(),
getSelectedTabMetadata: sandbox.stub()
@ -453,6 +456,22 @@ describe("loop.panel", function() {
} 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) {}
});
});
});
@ -930,4 +949,32 @@ describe("loop.panel", function() {
});
});
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);
});
});
});

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

@ -9,6 +9,7 @@ const kFxAKeyPref = "loop.key.fxa";
do_register_cleanup(function() {
Services.prefs.clearUserPref(kGuestKeyPref);
Services.prefs.clearUserPref(kFxAKeyPref);
MozLoopServiceInternal.fxAOAuthTokenData = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
});
@ -58,3 +59,26 @@ add_task(function* test_fxaGetKey() {
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");
});

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

@ -132,6 +132,7 @@ navigator.mozLoop = {
return false;
}
},
hasEncryptionKey: true,
setLoopPref: function(){},
releaseCallData: function() {},
copyString: function() {},

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

@ -17,6 +17,7 @@
// 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;
@ -265,6 +266,9 @@
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"},

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

@ -17,6 +17,7 @@
// 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;
@ -265,6 +266,9 @@
<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"}}

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

@ -196,6 +196,7 @@ let tests = [
};
MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
Services.prefs.setCharPref("loop.key.fxa", "fake");
yield MozLoopServiceInternal.notifyStatusChanged("login");
// Show the Loop menu.
@ -225,6 +226,7 @@ let tests = [
// 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;

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

@ -12,6 +12,15 @@ 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