Bug 1188771 - Inform users when they can't use the Hello service due to ToS compliance. r=mikedeboer

This commit is contained in:
Mark Banner 2015-09-25 12:57:24 +01:00
parent 670f9899d9
commit 41c6ed66ad
11 changed files with 194 additions and 63 deletions

View File

@ -401,6 +401,9 @@ loop.conversationViews = (function(mozL10n) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("generic_failure_message");
}

View File

@ -401,6 +401,9 @@ loop.conversationViews = (function(mozL10n) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("generic_failure_message");
}

View File

@ -917,25 +917,38 @@ loop.OTSdkDriver = (function() {
},
_onOTException: function(event) {
if (event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
event.message === "GetUserMedia") {
// We free up the publisher here in case the store wants to try
// grabbing the media again.
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
} else if (event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH) {
// We need to log the message so that we can understand where the exception
// is coming from. Potentially a temporary addition.
this._notifyMetricsEvent("sdk.exception." + event.code + "." + event.message);
} else {
this._notifyMetricsEvent("sdk.exception." + event.code);
switch (event.code) {
case OT.ExceptionCodes.UNABLE_TO_PUBLISH:
if (event.message === "GetUserMedia") {
// We free up the publisher here in case the store wants to try
// grabbing the media again.
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
// No exception logging as this is a handled event.
} else {
// We need to log the message so that we can understand where the exception
// is coming from. Potentially a temporary addition.
this._notifyMetricsEvent("sdk.exception." + event.code + "." + event.message);
}
break;
case OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE:
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.TOS_FAILURE
}));
// We still need to log the exception so that the server knows why this
// attempt failed.
this._notifyMetricsEvent("sdk.exception." + event.code);
break;
default:
this._notifyMetricsEvent("sdk.exception." + event.code);
break;
}
},

View File

@ -80,6 +80,9 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
COULD_NOT_CONNECT: "reason-could-not-connect",
NETWORK_DISCONNECTED: "reason-network-disconnected",
EXPIRED_OR_INVALID: "reason-expired-or-invalid",
// TOS_FAILURE reflects the sdk error code 1026:
// https://tokbox.com/developer/sdks/js/reference/ExceptionEvent.html
TOS_FAILURE: "reason-tos-failure",
UNKNOWN: "reason-unknown"
};

View File

@ -43,6 +43,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("rooms_media_denied_message");
case FAILURE_DETAILS.EXPIRED_OR_INVALID:
return mozL10n.get("rooms_unavailable_notification_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("status_error");
}
@ -52,7 +55,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
* This renders a retry button if one is necessary.
*/
renderRetryButton: function() {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID ||
this.props.failureReason === FAILURE_DETAILS.TOS_FAILURE) {
return null;
}
@ -593,6 +597,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
return {
StandaloneRoomFailureView: StandaloneRoomFailureView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomInfoArea: StandaloneRoomInfoArea,

View File

@ -43,6 +43,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("rooms_media_denied_message");
case FAILURE_DETAILS.EXPIRED_OR_INVALID:
return mozL10n.get("rooms_unavailable_notification_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("status_error");
}
@ -52,7 +55,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
* This renders a retry button if one is necessary.
*/
renderRetryButton: function() {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID ||
this.props.failureReason === FAILURE_DETAILS.TOS_FAILURE) {
return null;
}
@ -593,6 +597,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
return {
StandaloneRoomFailureView: StandaloneRoomFailureView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomInfoArea: StandaloneRoomInfoArea,

View File

@ -3,6 +3,10 @@ conversation_has_ended=Your conversation has ended.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
## as this will be replaced by the shortname.
tos_failure_message={{clientShortname}} is not available in your country.
retry_call_button=Retry
unable_retrieve_call_info=Unable to retrieve conversation information.
hangup_button_title=Hang up

View File

@ -307,6 +307,16 @@ describe("loop.conversationViews", function () {
expect(message.textContent).eql("contact_unavailable_title");
});
it("should display a ToS failure message for the ToS failure reason", function() {
view = mountTestComponent({
failureReason: FAILURE_DETAILS.TOS_FAILURE
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("tos_failure_message");
});
it("should display a generic unavailable message if the contact doesn't have a display name", function() {
view = mountTestComponent({
contact: {

View File

@ -68,6 +68,7 @@ describe("loop.OTSdkDriver", function () {
window.OT = {
ExceptionCodes: {
CONNECT_FAILED: 1006,
TERMS_OF_SERVICE_FAILURE: 1026,
UNABLE_TO_PUBLISH: 1500
}
};
@ -1599,6 +1600,38 @@ describe("loop.OTSdkDriver", function () {
}));
});
});
describe("ToS Failure", function() {
it("should dispatch a ConnectionFailure action", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
message: "Fake"
});
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.TOS_FAILURE
}));
});
it("should notify metrics", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
message: "Fake"
});
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionStatus({
event: "sdk.exception." + OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
state: "starting",
connections: 0,
sendStreams: 0,
recvStreams: 0
}));
});
});
});
});

View File

@ -17,7 +17,7 @@ describe("loop.standaloneRoomViews", function() {
var fixtures = document.querySelector("#fixtures");
var sandbox, dispatcher, activeRoomStore, dispatch;
var clock, fakeWindow;
var clock, fakeWindow, view;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -63,6 +63,7 @@ describe("loop.standaloneRoomViews", function() {
sandbox.restore();
clock.restore();
React.unmountComponentAtNode(fixtures);
view = null;
});
describe("StandaloneRoomHeader", function() {
@ -75,7 +76,7 @@ describe("loop.standaloneRoomViews", function() {
}
it("should dispatch a RecordClick action when the support link is clicked", function() {
var view = mountTestComponent();
view = mountTestComponent();
TestUtils.Simulate.click(view.getDOMNode().querySelector("a"));
@ -87,13 +88,93 @@ describe("loop.standaloneRoomViews", function() {
});
});
describe("StandaloneRoomFailureView", function() {
function mountTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomFailureView, props));
}
beforeEach(function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
});
it("should display a status error message if not reason is supplied", function() {
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("status_error");
});
it("should display a denied message on MEDIA_DENIED", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.MEDIA_DENIED });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("rooms_media_denied_message");
});
it("should display a denied message on NO_MEDIA", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.NO_MEDIA });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("rooms_media_denied_message");
});
it("should display an unavailable message on EXPIRED_OR_INVALID", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("rooms_unavailable_notification_message");
});
it("should display an tos failure message on TOS_FAILURE", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.TOS_FAILURE });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("tos_failure_message");
});
it("should not display a retry button when the failure reason is expired or invalid", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID });
expect(view.getDOMNode().querySelector(".btn-info")).eql(null);
});
it("should not display a retry button when the failure reason is tos failure", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.TOS_FAILURE });
expect(view.getDOMNode().querySelector(".btn-info")).eql(null);
});
it("should display a retry button for any other reason", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.NO_MEDIA });
expect(view.getDOMNode().querySelector(".btn-info")).not.eql(null);
});
it("should dispatch a RetryAfterRoomFailure action when the retry button is pressed", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.NO_MEDIA });
var button = view.getDOMNode().querySelector(".btn-info");
TestUtils.Simulate.click(button);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RetryAfterRoomFailure());
});
});
describe("StandaloneRoomInfoArea in fixture", function() {
it("should dispatch a RecordClick action when the tile is clicked", function(done) {
// Point the iframe to a page that will auto-"click"
loop.config.tilesIframeUrl = "data:text/html,<script>parent.postMessage('tile-click', '*');</script>";
// Render the iframe into the fixture to cause it to load
var view = React.render(
view = React.render(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomInfoArea, {
activeRoomStore: activeRoomStore,
@ -135,7 +216,7 @@ describe("loop.standaloneRoomViews", function() {
}));
}
function expectActionDispatched(view) {
function expectActionDispatched() {
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
@ -144,7 +225,7 @@ describe("loop.standaloneRoomViews", function() {
describe("#componentWillUpdate", function() {
it("should set document.title to roomName and brand name when the READY state is dispatched", function() {
activeRoomStore.setStoreState({roomName: "fakeName", roomState: ROOM_STATES.INIT});
var view = mountTestComponent();
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
expect(fakeWindow.document.title).to.equal("fakeName — clientShortname2");
@ -153,7 +234,7 @@ describe("loop.standaloneRoomViews", function() {
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
"is entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
var view = mountTestComponent();
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
@ -163,7 +244,7 @@ describe("loop.standaloneRoomViews", function() {
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
"re-entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
var view = mountTestComponent();
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
@ -172,8 +253,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#componentDidUpdate", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
@ -223,8 +302,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#componentWillReceiveProps", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
@ -280,8 +357,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#publishStream", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
view.setState({
@ -314,8 +389,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#render", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
@ -424,35 +497,11 @@ describe("loop.standaloneRoomViews", function() {
});
describe("Failed room message", function() {
beforeEach(function() {
it("should display the StandaloneRoomFailureView", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
});
it("should display a failed room message on FAILED", function() {
expect(view.getDOMNode().querySelector(".failed-room-message"))
.not.eql(null);
});
it("should display a retry button", function() {
expect(view.getDOMNode().querySelector(".btn-info")).not.eql(null);
});
it("should not display a retry button when the failure reason is expired or invalid", function() {
activeRoomStore.setStoreState({
failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID
});
expect(view.getDOMNode().querySelector(".btn-info")).eql(null);
});
it("should dispatch a RetryAfterRoomFailure action when the retry button is pressed", function() {
var button = view.getDOMNode().querySelector(".btn-info");
TestUtils.Simulate.click(button);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RetryAfterRoomFailure());
TestUtils.findRenderedComponentWithType(view,
loop.standaloneRoomViews.StandaloneRoomFailureView);
});
});

View File

@ -280,6 +280,9 @@ generic_contact_unavailable_title=This person is unavailable.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
## as this will be replaced by the shortname.
tos_failure_message={{clientShortname}} is not available in your country.
## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
## contact is offline.