From 41c6ed66ada4c035476c44d03e3f48d606b97a56 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Fri, 25 Sep 2015 12:57:24 +0100 Subject: [PATCH] Bug 1188771 - Inform users when they can't use the Hello service due to ToS compliance. r=mikedeboer --- .../loop/content/js/conversationViews.js | 3 + .../loop/content/js/conversationViews.jsx | 3 + .../loop/content/shared/js/otSdkDriver.js | 51 ++++--- .../loop/content/shared/js/utils.js | 3 + .../content/js/standaloneRoomViews.js | 7 +- .../content/js/standaloneRoomViews.jsx | 7 +- .../content/l10n/en-US/loop.properties | 4 + .../desktop-local/conversationViews_test.js | 10 ++ .../loop/test/shared/otSdkDriver_test.js | 33 +++++ .../standalone/standaloneRoomViews_test.js | 133 ++++++++++++------ .../en-US/chrome/browser/loop/loop.properties | 3 + 11 files changed, 194 insertions(+), 63 deletions(-) diff --git a/browser/components/loop/content/js/conversationViews.js b/browser/components/loop/content/js/conversationViews.js index ad402aff10e..d91282096cb 100644 --- a/browser/components/loop/content/js/conversationViews.js +++ b/browser/components/loop/content/js/conversationViews.js @@ -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"); } diff --git a/browser/components/loop/content/js/conversationViews.jsx b/browser/components/loop/content/js/conversationViews.jsx index 2f5b3dd1d6a..7a28d794554 100644 --- a/browser/components/loop/content/js/conversationViews.jsx +++ b/browser/components/loop/content/js/conversationViews.jsx @@ -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"); } diff --git a/browser/components/loop/content/shared/js/otSdkDriver.js b/browser/components/loop/content/shared/js/otSdkDriver.js index d74bcc0e0e5..fa32471a0c0 100644 --- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -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; } }, diff --git a/browser/components/loop/content/shared/js/utils.js b/browser/components/loop/content/shared/js/utils.js index b18005f036d..370f8aaa727 100644 --- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -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" }; diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js index f9acb4e522c..44a54db7bf5 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -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, diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx index 8b7170c25ef..0e1bab9d4cb 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -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, diff --git a/browser/components/loop/standalone/content/l10n/en-US/loop.properties b/browser/components/loop/standalone/content/l10n/en-US/loop.properties index e533fe4295f..f054ec58159 100644 --- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties +++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties @@ -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 diff --git a/browser/components/loop/test/desktop-local/conversationViews_test.js b/browser/components/loop/test/desktop-local/conversationViews_test.js index 24680f61ea9..d58ecb59acf 100644 --- a/browser/components/loop/test/desktop-local/conversationViews_test.js +++ b/browser/components/loop/test/desktop-local/conversationViews_test.js @@ -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: { diff --git a/browser/components/loop/test/shared/otSdkDriver_test.js b/browser/components/loop/test/shared/otSdkDriver_test.js index 6acb6589906..679b59479ec 100644 --- a/browser/components/loop/test/shared/otSdkDriver_test.js +++ b/browser/components/loop/test/shared/otSdkDriver_test.js @@ -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 + })); + }); + }); }); }); diff --git a/browser/components/loop/test/standalone/standaloneRoomViews_test.js b/browser/components/loop/test/standalone/standaloneRoomViews_test.js index 796622a71f7..398acc32bc0 100644 --- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -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,"; // 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); }); }); diff --git a/browser/locales/en-US/chrome/browser/loop/loop.properties b/browser/locales/en-US/chrome/browser/loop/loop.properties index 38d0c349e4d..db64b985d3d 100644 --- a/browser/locales/en-US/chrome/browser/loop/loop.properties +++ b/browser/locales/en-US/chrome/browser/loop/loop.properties @@ -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.