Bug 1171415 - Implement NPS feedback form for Loop. r=mikedeboer
authorAndrei Oprea <andrei.br92@gmail.com>
Mon, 20 Jul 2015 10:47:00 -0400
changeset 279916 c29c62b67e07a39ac0586fdfa3ef47bd71a479fa
parent 279915 b8487544a3c8c7677c65f8dfe56fac9a27dcfa7f
child 279917 2f28e9e0009ad62462063ee73c3ba43732ede460
push id3668
push usermconley@mozilla.com
push dateTue, 21 Jul 2015 21:35:10 +0000
reviewersmikedeboer
bugs1171415
milestone42.0a1
Bug 1171415 - Implement NPS feedback form for Loop. r=mikedeboer
browser/app/profile/firefox.js
browser/components/loop/.eslintignore
browser/components/loop/content/conversation.html
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/conversationAppStore.js
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/js/feedbackViews.js
browser/components/loop/content/js/feedbackViews.jsx
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/img/helloicon.svg
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/feedbackApiClient.js
browser/components/loop/content/shared/js/feedbackStore.js
browser/components/loop/content/shared/js/feedbackViews.js
browser/components/loop/content/shared/js/feedbackViews.jsx
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/jar.mn
browser/components/loop/standalone/content/css/webapp.css
browser/components/loop/standalone/content/index.html
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/standalone/content/js/webapp.js
browser/components/loop/standalone/content/js/webapp.jsx
browser/components/loop/standalone/content/l10n/en-US/loop.properties
browser/components/loop/test/desktop-local/conversationAppStore_test.js
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/desktop-local/feedbackViews_test.js
browser/components/loop/test/desktop-local/index.html
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/test/functional/test_1_browser_call.py
browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/shared/feedbackApiClient_test.js
browser/components/loop/test/shared/feedbackStore_test.js
browser/components/loop/test/shared/feedbackViews_test.js
browser/components/loop/test/shared/index.html
browser/components/loop/test/standalone/index.html
browser/components/loop/test/standalone/standaloneRoomViews_test.js
browser/components/loop/test/standalone/webapp_test.js
browser/components/loop/ui/index.html
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1758,16 +1758,19 @@ pref("loop.ping.interval", 1800000);
 pref("loop.ping.timeout", 10000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.loglevel", "Error");
 pref("loop.debug.dispatcher", false);
 pref("loop.debug.websocket", false);
 pref("loop.debug.sdk", false);
 pref("loop.debug.twoWayMediaTelemetry", false);
+pref("loop.feedback.dateLastSeenSec", 0);
+pref("loop.feedback.periodSec", 15770000); // 6 months.
+pref("loop.feedback.formURL", "http://www.surveygizmo.com/s3/2227372/Firefox-Hello-Product-Survey");
 #ifdef DEBUG
 pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
 #else
 pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
 #endif
 pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
 pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
 pref("loop.fxa_oauth.tokendata", "");
--- a/browser/components/loop/.eslintignore
+++ b/browser/components/loop/.eslintignore
@@ -12,15 +12,15 @@ standalone/node_modules
 # Libs we don't need to check
 test/shared/vendor
 # These are generated react files that we don't need to check
 content/js/contacts.js
 content/js/conversation.js
 content/js/conversationViews.js
 content/js/panel.js
 content/js/roomViews.js
-content/shared/js/feedbackViews.js
+content/js/feedbackViews.js
 content/shared/js/textChatView.js
 content/shared/js/views.js
 standalone/content/js/fxOSMarketplace.js
 standalone/content/js/standaloneRoomViews.js
 standalone/content/js/webapp.js
 ui/ui-showcase.js
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -22,29 +22,27 @@
     <script type="text/javascript" src="loop/libs/sdk.js"></script>
     <script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.4.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
-    <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="loop/shared/js/store.js"></script>
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
     <script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
-    <script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
-    <script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>
+    <script type="text/javascript" src="loop/js/feedbackViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/js/roomStore.js"></script>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -1,60 +1,91 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
   var CallControllerView = loop.conversationViews.CallControllerView;
-  var CallIdentifierView = loop.conversationViews.CallIdentifierView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({displayName: "AppControllerView",
     mixins: [
       Backbone.Events,
       loop.store.StoreMixin("conversationAppStore"),
+      sharedMixins.DocumentTitleMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
+    _renderFeedbackForm: function() {
+      this.setTitle(mozL10n.get("conversation_has_ended"));
+
+      return (React.createElement(FeedbackView, {
+        mozLoop: this.props.mozLoop, 
+        onAfterFeedbackReceived: this.closeWindow}));
+    },
+
+    /**
+     * We only show the feedback for once every 6 months, otherwise close
+     * the window.
+     */
+    handleCallTerminated: function() {
+      var delta = new Date() - new Date(this.state.feedbackTimestamp);
+
+      // Show timestamp if feedback period (6 months) passed.
+      // 0 is default value for pref. Always show feedback form on first use.
+      if (this.state.feedbackTimestamp === 0 ||
+          delta >= this.state.feedbackPeriod) {
+        this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+        return;
+      }
+
+      this.closeWindow();
+    },
+
     render: function() {
+      if (this.state.showFeedbackForm) {
+        return this._renderFeedbackForm();
+      }
+
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (React.createElement(CallControllerView, {
             dispatcher: this.props.dispatcher, 
-            mozLoop: this.props.mozLoop}));
+            mozLoop: this.props.mozLoop, 
+            onCallTerminated: this.handleCallTerminated}));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
+            onCallTerminated: this.handleCallTerminated, 
             roomStore: this.props.roomStore}));
         }
         case "failed": {
           return React.createElement(GenericFailureView, {cancelCall: this.closeWindow});
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
@@ -97,25 +128,16 @@ loop.conversation = (function(mozL10n) {
       dispatcher: dispatcher,
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
-    var appVersionInfo = navigator.mozLoop.appVersionInfo;
-    var feedbackClient = new loop.FeedbackAPIClient(
-      navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
-      product: navigator.mozLoop.getLoopPref("feedback.product"),
-      platform: appVersionInfo.OS,
-      channel: appVersionInfo.channel,
-      version: appVersionInfo.version
-    });
-
     // Create the stores.
     var conversationAppStore = new loop.store.ConversationAppStore({
       dispatcher: dispatcher,
       mozLoop: navigator.mozLoop
     });
     var conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: client,
       isDesktop: true,
@@ -126,27 +148,23 @@ loop.conversation = (function(mozL10n) {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       conversationAppStore: conversationAppStore,
       conversationStore: conversationStore,
-      feedbackStore: feedbackStore,
       textChatStore: textChatStore
     });
 
     // Obtain the windowId and pass it through
     var locationHash = loop.shared.utils.locationData().hash;
     var windowId;
 
     var hash = locationHash.match(/#(.*)/);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -1,60 +1,91 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
   var CallControllerView = loop.conversationViews.CallControllerView;
-  var CallIdentifierView = loop.conversationViews.CallIdentifierView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({
     mixins: [
       Backbone.Events,
       loop.store.StoreMixin("conversationAppStore"),
+      sharedMixins.DocumentTitleMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
+    _renderFeedbackForm: function() {
+      this.setTitle(mozL10n.get("conversation_has_ended"));
+
+      return (<FeedbackView
+        mozLoop={this.props.mozLoop}
+        onAfterFeedbackReceived={this.closeWindow} />);
+    },
+
+    /**
+     * We only show the feedback for once every 6 months, otherwise close
+     * the window.
+     */
+    handleCallTerminated: function() {
+      var delta = new Date() - new Date(this.state.feedbackTimestamp);
+
+      // Show timestamp if feedback period (6 months) passed.
+      // 0 is default value for pref. Always show feedback form on first use.
+      if (this.state.feedbackTimestamp === 0 ||
+          delta >= this.state.feedbackPeriod) {
+        this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+        return;
+      }
+
+      this.closeWindow();
+    },
+
     render: function() {
+      if (this.state.showFeedbackForm) {
+        return this._renderFeedbackForm();
+      }
+
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (<CallControllerView
             dispatcher={this.props.dispatcher}
-            mozLoop={this.props.mozLoop} />);
+            mozLoop={this.props.mozLoop}
+            onCallTerminated={this.handleCallTerminated} />);
         }
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
+            onCallTerminated={this.handleCallTerminated}
             roomStore={this.props.roomStore} />);
         }
         case "failed": {
           return <GenericFailureView cancelCall={this.closeWindow} />;
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
@@ -97,25 +128,16 @@ loop.conversation = (function(mozL10n) {
       dispatcher: dispatcher,
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
-    var appVersionInfo = navigator.mozLoop.appVersionInfo;
-    var feedbackClient = new loop.FeedbackAPIClient(
-      navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
-      product: navigator.mozLoop.getLoopPref("feedback.product"),
-      platform: appVersionInfo.OS,
-      channel: appVersionInfo.channel,
-      version: appVersionInfo.version
-    });
-
     // Create the stores.
     var conversationAppStore = new loop.store.ConversationAppStore({
       dispatcher: dispatcher,
       mozLoop: navigator.mozLoop
     });
     var conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: client,
       isDesktop: true,
@@ -126,27 +148,23 @@ loop.conversation = (function(mozL10n) {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       conversationAppStore: conversationAppStore,
       conversationStore: conversationStore,
-      feedbackStore: feedbackStore,
       textChatStore: textChatStore
     });
 
     // Obtain the windowId and pass it through
     var locationHash = loop.shared.utils.locationData().hash;
     var windowId;
 
     var hash = locationHash.match(/#(.*)/);
--- a/browser/components/loop/content/js/conversationAppStore.js
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -22,24 +22,36 @@ loop.store.ConversationAppStore = (funct
       throw new Error("Missing option dispatcher");
     }
     if (!options.mozLoop) {
       throw new Error("Missing option mozLoop");
     }
 
     this._dispatcher = options.dispatcher;
     this._mozLoop = options.mozLoop;
-    this._storeState = {};
+    this._storeState = this.getInitialStoreState();
 
     this._dispatcher.register(this, [
-      "getWindowData"
+      "getWindowData",
+      "showFeedbackForm"
     ]);
   };
 
   ConversationAppStore.prototype = _.extend({
+    getInitialStoreState: function() {
+      return {
+        // How often to display the form. Convert seconds to ms.
+        feedbackPeriod: this._mozLoop.getLoopPref("feedback.periodSec") * 1000,
+        // Date when the feedback form was last presented. Convert to ms.
+        feedbackTimestamp: this._mozLoop
+                               .getLoopPref("feedback.dateLastSeenSec") * 1000,
+        showFeedbackForm: false
+      };
+    },
+
     /**
      * Retrieves current store state.
      *
      * @return {Object}
      */
     getStoreState: function() {
       return this._storeState;
     },
@@ -50,16 +62,30 @@ loop.store.ConversationAppStore = (funct
      * @param {Object} state The new store state.
      */
     setStoreState: function(state) {
       this._storeState = state;
       this.trigger("change");
     },
 
     /**
+     * Sets store state which will result in the feedback form rendered.
+     * Saves a timestamp of when the feedback was last rendered.
+     */
+    showFeedbackForm: function() {
+      var timestamp = Math.floor(new Date().getTime() / 1000);
+
+      this._mozLoop.setLoopPref("feedback.dateLastSeenSec", timestamp);
+
+      this.setStoreState({
+        showFeedbackForm: true
+      });
+    },
+
+    /**
      * Handles the get window data action - obtains the window data,
      * updates the store and notifies interested components.
      *
      * @param {sharedActions.GetWindowData} actionData The action data
      */
     getWindowData: function(actionData) {
       var windowData = this._mozLoop.getConversationWindowData(actionData.windowId);
 
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -10,17 +10,16 @@ loop.conversationViews = (function(mozL1
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
 
   // XXXdmose this code is already out of sync with the code in contacts.jsx
   // which, unlike this code, now has unit tests.  We should totally do the
   // above.
@@ -696,17 +695,18 @@ loop.conversationViews = (function(mozL1
       sharedMixins.AudioMixin,
       sharedMixins.DocumentTitleMixin,
       loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired
+      mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
@@ -715,29 +715,16 @@ loop.conversationViews = (function(mozL1
     /**
      * Returns true if the call is in a cancellable state, during call setup.
      */
     _isCancellable: function() {
       return this.state.callState !== CALL_STATES.INIT &&
              this.state.callState !== CALL_STATES.GATHER;
     },
 
-    /**
-     * Used to setup and render the feedback view.
-     */
-    _renderFeedbackView: function() {
-      this.setTitle(mozL10n.get("conversation_has_ended"));
-
-      return (
-        React.createElement(sharedViews.FeedbackView, {
-          onAfterFeedbackReceived: this._closeWindow.bind(this)}
-        )
-      );
-    },
-
     _renderViewFromCallType: function() {
       // For outgoing calls we can display the pending conversation view
       // for any state that render() doesn't manage.
       if (this.state.outgoing) {
         return (React.createElement(PendingConversationView, {
           callState: this.state.callState, 
           contact: this.state.contact, 
           dispatcher: this.props.dispatcher, 
@@ -755,16 +742,24 @@ loop.conversationViews = (function(mozL1
         ));
       }
 
       // Otherwise we're still gathering or connecting, so
       // don't display anything.
       return null;
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.callState === CALL_STATES.ONGOING &&
+          this.state.callState === CALL_STATES.FINISHED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       // Set the default title to the contact name or the callerId, note
       // that views may override this, e.g. the feedback view.
       if (this.state.contact) {
         this.setTitle(_getContactDisplayName(this.state.contact));
       } else {
         this.setTitle(this.state.callerId || "");
       }
@@ -787,17 +782,20 @@ loop.conversationViews = (function(mozL1
             mediaConnected: this.state.mediaConnected, 
             remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
             remoteVideoEnabled: this.state.remoteVideoEnabled, 
             video: {enabled: !this.state.videoMuted}})
           );
         }
         case CALL_STATES.FINISHED: {
           this.play("terminated");
-          return this._renderFeedbackView();
+
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         case CALL_STATES.INIT: {
           // We know what we are, but we haven't got the data yet.
           return null;
         }
         default: {
           return this._renderViewFromCallType();
         }
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -10,17 +10,16 @@ loop.conversationViews = (function(mozL1
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
 
   // XXXdmose this code is already out of sync with the code in contacts.jsx
   // which, unlike this code, now has unit tests.  We should totally do the
   // above.
@@ -696,17 +695,18 @@ loop.conversationViews = (function(mozL1
       sharedMixins.AudioMixin,
       sharedMixins.DocumentTitleMixin,
       loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired
+      mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
@@ -715,29 +715,16 @@ loop.conversationViews = (function(mozL1
     /**
      * Returns true if the call is in a cancellable state, during call setup.
      */
     _isCancellable: function() {
       return this.state.callState !== CALL_STATES.INIT &&
              this.state.callState !== CALL_STATES.GATHER;
     },
 
-    /**
-     * Used to setup and render the feedback view.
-     */
-    _renderFeedbackView: function() {
-      this.setTitle(mozL10n.get("conversation_has_ended"));
-
-      return (
-        <sharedViews.FeedbackView
-          onAfterFeedbackReceived={this._closeWindow.bind(this)}
-        />
-      );
-    },
-
     _renderViewFromCallType: function() {
       // For outgoing calls we can display the pending conversation view
       // for any state that render() doesn't manage.
       if (this.state.outgoing) {
         return (<PendingConversationView
           callState={this.state.callState}
           contact={this.state.contact}
           dispatcher={this.props.dispatcher}
@@ -755,16 +742,24 @@ loop.conversationViews = (function(mozL1
         />);
       }
 
       // Otherwise we're still gathering or connecting, so
       // don't display anything.
       return null;
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.callState === CALL_STATES.ONGOING &&
+          this.state.callState === CALL_STATES.FINISHED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       // Set the default title to the contact name or the callerId, note
       // that views may override this, e.g. the feedback view.
       if (this.state.contact) {
         this.setTitle(_getContactDisplayName(this.state.contact));
       } else {
         this.setTitle(this.state.callerId || "");
       }
@@ -787,17 +782,20 @@ loop.conversationViews = (function(mozL1
             mediaConnected={this.state.mediaConnected}
             remoteSrcVideoObject={this.state.remoteSrcVideoObject}
             remoteVideoEnabled={this.state.remoteVideoEnabled}
             video={{enabled: !this.state.videoMuted}} />
           );
         }
         case CALL_STATES.FINISHED: {
           this.play("terminated");
-          return this._renderFeedbackView();
+
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         case CALL_STATES.INIT: {
           // We know what we are, but we haven't got the data yet.
           return null;
         }
         default: {
           return this._renderViewFromCallType();
         }
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/feedbackViews.js
@@ -0,0 +1,47 @@
+var loop = loop || {};
+loop.feedbackViews = (function(_, mozL10n) {
+  "use strict";
+
+  /**
+   * Feedback view is displayed once every 6 months (loop.feedback.periodSec)
+   * after a conversation has ended.
+   */
+  var FeedbackView = React.createClass({displayName: "FeedbackView",
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func.isRequired
+    },
+
+    /**
+     * Pressing the button to leave feedback will open the form in a new page
+     * and close the conversation window.
+     */
+    onFeedbackButtonClick: function() {
+      var url = this.props.mozLoop.getLoopPref("feedback.formURL");
+      this.props.mozLoop.openURL(url);
+
+      this.props.onAfterFeedbackReceived();
+    },
+
+    render: function() {
+      return (
+        React.createElement("div", {className: "feedback-view-container"}, 
+          React.createElement("h2", {className: "feedback-heading"}, 
+            mozL10n.get("feedback_window_heading")
+          ), 
+          React.createElement("div", {className: "feedback-hello-logo"}), 
+          React.createElement("div", {className: "feedback-button-container"}, 
+            React.createElement("button", {onClick: this.onFeedbackButtonClick, 
+              ref: "feedbackFormBtn"}, 
+              mozL10n.get("feedback_request_button")
+            )
+          )
+        )
+      );
+    }
+  });
+
+  return {
+    FeedbackView: FeedbackView
+  };
+})(_, navigator.mozL10n || document.mozL10n);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/feedbackViews.jsx
@@ -0,0 +1,47 @@
+var loop = loop || {};
+loop.feedbackViews = (function(_, mozL10n) {
+  "use strict";
+
+  /**
+   * Feedback view is displayed once every 6 months (loop.feedback.periodSec)
+   * after a conversation has ended.
+   */
+  var FeedbackView = React.createClass({
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func.isRequired
+    },
+
+    /**
+     * Pressing the button to leave feedback will open the form in a new page
+     * and close the conversation window.
+     */
+    onFeedbackButtonClick: function() {
+      var url = this.props.mozLoop.getLoopPref("feedback.formURL");
+      this.props.mozLoop.openURL(url);
+
+      this.props.onAfterFeedbackReceived();
+    },
+
+    render: function() {
+      return (
+        <div className="feedback-view-container">
+          <h2 className="feedback-heading">
+            {mozL10n.get("feedback_window_heading")}
+          </h2>
+          <div className="feedback-hello-logo" />
+          <div className="feedback-button-container">
+            <button onClick={this.onFeedbackButtonClick}
+              ref="feedbackFormBtn">
+              {mozL10n.get("feedback_request_button")}
+            </button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  return {
+    FeedbackView: FeedbackView
+  };
+})(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -553,16 +553,17 @@ loop.roomViews = (function(mozL10n) {
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
         contextEnabled: this.props.mozLoop.getLoopPref("contextInConversations.enabled"),
         showEditContext: false
@@ -688,16 +689,24 @@ loop.roomViews = (function(mozL10n) {
     handleEditContextClick: function() {
       this.setState({ showEditContext: !this.state.showEditContext });
     },
 
     handleEditContextClose: function() {
       this.setState({ showEditContext: false });
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.roomState === ROOM_STATES.ENDED &&
+          this.state.roomState === ROOM_STATES.ENDED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
 
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
@@ -721,20 +730,19 @@ loop.roomViews = (function(mozL10n) {
           //       FULL case should never happen on desktop.
           return (
             React.createElement(loop.conversationViews.GenericFailureView, {
               cancelCall: this.closeWindow, 
               failureReason: this.state.failureReason})
           );
         }
         case ROOM_STATES.ENDED: {
-          return (
-            React.createElement(sharedViews.FeedbackView, {
-              onAfterFeedbackReceived: this.closeWindow})
-          );
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         default: {
 
           return (
             React.createElement("div", {className: "room-conversation-wrapper"}, 
               React.createElement("div", {className: "video-layout-wrapper"}, 
                 React.createElement("div", {className: "conversation room-conversation"}, 
                   React.createElement("div", {className: "media nested"}, 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -553,16 +553,17 @@ loop.roomViews = (function(mozL10n) {
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
         contextEnabled: this.props.mozLoop.getLoopPref("contextInConversations.enabled"),
         showEditContext: false
@@ -688,16 +689,24 @@ loop.roomViews = (function(mozL10n) {
     handleEditContextClick: function() {
       this.setState({ showEditContext: !this.state.showEditContext });
     },
 
     handleEditContextClose: function() {
       this.setState({ showEditContext: false });
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.roomState === ROOM_STATES.ENDED &&
+          this.state.roomState === ROOM_STATES.ENDED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
 
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
@@ -721,20 +730,19 @@ loop.roomViews = (function(mozL10n) {
           //       FULL case should never happen on desktop.
           return (
             <loop.conversationViews.GenericFailureView
               cancelCall={this.closeWindow}
               failureReason={this.state.failureReason} />
           );
         }
         case ROOM_STATES.ENDED: {
-          return (
-            <sharedViews.FeedbackView
-              onAfterFeedbackReceived={this.closeWindow} />
-          );
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         default: {
 
           return (
             <div className="room-conversation-wrapper">
               <div className="video-layout-wrapper">
                 <div className="conversation room-conversation">
                   <div className="media nested">
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -418,66 +418,57 @@
   margin: 2em 0;
 }
 
 .promote-firefox h3 {
   font-weight: 300;
 }
 
 /* Feedback form */
-
-.feedback {
-  padding: 14px;
-}
-
-.feedback p {
-  margin: 0px;
+.feedback-view-container {
+  display: flex;
+  flex-direction: column;
+  flex-wrap: nowrap;
+  justify-content: center;
+  align-content: center;
+  align-items: flex-start;
+  height: 100%;
 }
 
-.feedback h3 {
-  color: #666;
-  font-size: 12px;
-  font-weight: 700;
+.feedback-heading {
+  margin: 1em 0;
+  width: 100%;
   text-align: center;
-  margin: 0 0 1em 0;
-}
-
-.feedback .faces {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  padding: 20px 0;
+  font-weight: bold;
+  font-size: 1.2em;
 }
 
-.feedback .face {
-  border: 1px solid transparent;
-  box-shadow: 0 1px 2px #CCC;
-  cursor: pointer;
-  border-radius: 4px;
-  margin: 0 10px;
-  width: 80px;
-  height: 80px;
-  background-color: #fbfbfb;
-  background-size: 60px auto;
-  background-position: center center;
+.feedback-hello-logo {
+  background-image: url("../img/helloicon.svg");
+  background-position: center;
+  background-size: contain;
   background-repeat: no-repeat;
+  flex: 2 1 auto;
+  width: 100%;
+  margin: 30px 0;
 }
 
-.feedback .face:hover {
-  border: 1px solid #DDD;
-  background-color: #FEFEFE;
+.feedback-button-container {
+  flex: 0 1 auto;
+  margin: 30px;
+  align-self: center;
 }
 
-.feedback .face.face-happy {
-  background-image: url("../img/happy.png");
-}
-
-.feedback .face.face-sad {
-  background-image: url("../img/sad.png");
+.feedback-button-container button {
+  margin: 0 30px;
+  padding: .5em 2em;
+  border: none;
+  background: #4E92DF;
+  color: #fff;
+  cursor: pointer;
 }
 
 .fx-embedded-btn-back {
   margin-bottom: 1rem;
   padding: .2rem .8rem;
   border: 1px solid #aaa;
   border-radius: 2px;
   background: transparent;
@@ -1542,34 +1533,29 @@ html[dir="rtl"] .text-chat-entry.receive
 }
 
 /* Text chat entry timestamp */
 .text-chat-entry-timestamp {
   margin: 0 .5em;
   color: #aaa;
   font-style: italic;
   font-size: .8em;
-  order: 0;
   flex: 0 1 auto;
   align-self: center;
 }
 
 /* Sent text chat entries should be on the right */
 .text-chat-entry.sent {
   justify-content: flex-end;
 }
 
 .received > .text-chat-entry-timestamp {
   order: 2;
 }
 
-.sent > .text-chat-entry-timestamp {
-  order: 0;
-}
-
 /* Pseudo element used to cover part between chat bubble and chat arrow. */
 .text-chat-entry > p:after {
   position: absolute;
   background: #fff;
   content: "";
 }
 
 .text-chat-entry.sent > p:after {
@@ -1626,17 +1612,16 @@ html[dir="rtl"] .text-chat-entry.receive
   align-self: flex-end;
 }
 
 .text-chat-entry.received .text-chat-arrow {
   margin-left: 0;
   margin-right: -9px;
   height: 10px;
   background-image: url("../img/chatbubble-arrow-left.svg");
-  order: 0;
   align-self: auto;
 }
 
 html[dir="rtl"] .text-chat-arrow {
   transform: scaleX(-1);
 }
 
 html[dir="rtl"] .text-chat-entry.sent .text-chat-arrow {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/helloicon.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="evenodd" clip-rule="evenodd" fill="#4E92DF" d="M32 0C14.3 0 0 12.6 0 28.1c0 7.7 3.6 14.7 9.3 19.8-1 3.5-3 8.3-6.9 12.9.7 1.2 11.7-3 19.4-6.1 3.2.9 6.6 1.5 10.2 1.5 17.7 0 32-12.6 32-28.1S49.7 0 32 0zm9.6 16.9c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zm-19.3 0c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zM32 47.7h-.1-.1c-8.6 0-18.1-5.5-20.3-14.9 5.8 2.7 13.8 3.8 20.4 3.8 6.6 0 14.7-1.2 20.4-3.8-2.2 9.3-11.7 14.9-20.3 14.9z"/></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -516,62 +516,39 @@ loop.shared.actions = (function() {
     JoinedRoom: Action.define("joinedRoom", {
       apiKey: String,
       sessionToken: String,
       sessionId: String,
       expires: Number
     }),
 
     /**
-     * Used to indicate that the feedback cycle is completed and the countdown
-     * finished.
+     * Used to indicate the user wishes to leave the room.
      */
-    FeedbackComplete: Action.define("feedbackComplete", {
+    LeaveRoom: Action.define("leaveRoom", {
     }),
 
     /**
-     * Used to indicate the user wishes to leave the room.
+     * Signals that the feedback view should be rendered.
      */
-    LeaveRoom: Action.define("leaveRoom", {
+    ShowFeedbackForm: Action.define("showFeedbackForm", {
     }),
 
     /**
      * Used to record a link click for metrics purposes.
      */
     RecordClick: Action.define("recordClick", {
       // Note: for ToS and Privacy links, this should be the link, for
       // other links this should be a generic description so that we don't
       // record what users are clicking, just the information about the fact
       // they clicked the link in that spot (e.g. "Shared URL").
       linkInfo: String
     }),
 
     /**
-     * Requires detailed information on sad feedback.
-     */
-    RequireFeedbackDetails: Action.define("requireFeedbackDetails", {
-    }),
-
-    /**
-     * Send feedback data.
-     */
-    SendFeedback: Action.define("sendFeedback", {
-      happy: Boolean,
-      category: String,
-      description: String
-    }),
-
-    /**
-     * Reacts on feedback submission error.
-     */
-    SendFeedbackError: Action.define("sendFeedbackError", {
-      error: Error
-    }),
-
-    /**
      * Used to inform of the current session, publisher and connection
      * status.
      */
     ConnectionStatus: Action.define("connectionStatus", {
       event: String,
       state: String,
       connections: Number,
       sendStreams: Number,
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackApiClient.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.FeedbackAPIClient = (function($, _) {
-  "use strict";
-
-  /**
-   * Feedback API client. Sends feedback data to an input.mozilla.com compatible
-   * API.
-   *
-   * @param {String} baseUrl  Base API url (required)
-   * @param {Object} defaults Defaults field values for that client.
-   *
-   * Required defaults:
-   * - {String} product Product name (required)
-   *
-   * Optional defaults:
-   * - {String} platform   Platform name, eg. "Windows 8", "Android", "Linux"
-   * - {String} version    Product version, eg. "22b2", "1.1"
-   * - {String} channel    Product channel, eg. "stable", "beta"
-   * - {String} user_agent eg. Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0
-   *
-   * @link  http://fjord.readthedocs.org/en/latest/api.html
-   */
-  function FeedbackAPIClient(baseUrl, defaults) {
-    this.baseUrl = baseUrl;
-    if (!this.baseUrl) {
-      throw new Error("Missing required 'baseUrl' argument.");
-    }
-
-    this.defaults = defaults || {};
-    // required defaults checks
-    if (!this.defaults.hasOwnProperty("product")) {
-      throw new Error("Missing required 'product' default.");
-    }
-  }
-
-  FeedbackAPIClient.prototype = {
-    /**
-     * Supported field names by the feedback API.
-     * @type {Array}
-     */
-    _supportedFields: ["happy",
-                       "category",
-                       "description",
-                       "product",
-                       "platform",
-                       "version",
-                       "channel",
-                       "user_agent",
-                       "url"],
-
-    /**
-     * Creates a formatted payload object compliant with the Feedback API spec
-     * against validated field data.
-     *
-     * @param  {Object} fields Feedback initial values.
-     * @return {Object}        Formatted payload object.
-     * @throws {Error}         If provided values are invalid
-     */
-    _createPayload: function(fields) {
-      if (typeof fields !== "object") {
-        throw new Error("Invalid feedback data provided.");
-      }
-
-      Object.keys(fields).forEach(function(name) {
-        if (this._supportedFields.indexOf(name) === -1) {
-          throw new Error("Unsupported field " + name);
-        }
-      }, this);
-
-      // Payload is basically defaults + fields merged in
-      var payload = _.extend({}, this.defaults, fields);
-
-      // Default description field value
-      if (!fields.description) {
-        payload.description = (fields.happy ? "Happy" : "Sad") + " User";
-      }
-
-      return payload;
-    },
-
-    /**
-     * Sends feedback data.
-     *
-     * @param  {Object}   fields Feedback form data.
-     * @param  {Function} cb     Callback(err, result)
-     */
-    send: function(fields, cb) {
-      var req = $.ajax({
-        url: this.baseUrl,
-        method: "POST",
-        contentType: "application/json",
-        dataType: "json",
-        data: JSON.stringify(this._createPayload(fields))
-      });
-
-      req.done(function(result) {
-        console.info("User feedback data have been submitted", result);
-        cb(null, result);
-      });
-
-      req.fail(function(jqXHR, textStatus, errorThrown) {
-        var message = "Error posting user feedback data";
-        var httpError = jqXHR.status + " " + errorThrown;
-        cb(new Error(message + ": " + httpError + "; " +
-                     (jqXHR.responseJSON && jqXHR.responseJSON.detail || "")));
-      });
-    }
-  };
-
-  return FeedbackAPIClient;
-})(jQuery, _);
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackStore.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.store = loop.store || {};
-
-loop.store.FeedbackStore = (function() {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES = {
-    // Initial state (mood selection)
-    INIT: "feedback-init",
-    // User detailed feedback form step
-    DETAILS: "feedback-details",
-    // Pending feedback data submission
-    PENDING: "feedback-pending",
-    // Feedback has been sent
-    SENT: "feedback-sent",
-    // There was an issue with the feedback API
-    FAILED: "feedback-failed"
-  };
-
-  /**
-   * Feedback store.
-   *
-   * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
-   *                                      and registering to consume actions.
-   * @param {Object} options Options object:
-   * - {mozLoop}        mozLoop                 The MozLoop API object.
-   * - {feedbackClient} loop.FeedbackAPIClient  The feedback API client.
-   */
-  var FeedbackStore = loop.store.createStore({
-    actions: [
-      "requireFeedbackDetails",
-      "sendFeedback",
-      "sendFeedbackError",
-      "feedbackComplete"
-    ],
-
-    initialize: function(options) {
-      if (!options.feedbackClient) {
-        throw new Error("Missing option feedbackClient");
-      }
-      this._feedbackClient = options.feedbackClient;
-    },
-
-    /**
-     * Returns initial state data for this active room.
-     */
-    getInitialStoreState: function() {
-      return {feedbackState: FEEDBACK_STATES.INIT};
-    },
-
-    /**
-     * Requires user detailed feedback.
-     */
-    requireFeedbackDetails: function() {
-      this.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-    },
-
-    /**
-     * Sends feedback data to the feedback server.
-     *
-     * @param {sharedActions.SendFeedback} actionData The action data.
-     */
-    sendFeedback: function(actionData) {
-      delete actionData.name;
-      this._feedbackClient.send(actionData, function(err) {
-        if (err) {
-          this.dispatchAction(new sharedActions.SendFeedbackError({
-            error: err
-          }));
-          return;
-        }
-        this.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-      }.bind(this));
-
-      this.setStoreState({feedbackState: FEEDBACK_STATES.PENDING});
-    },
-
-    /**
-     * Notifies a store from any error encountered while sending feedback data.
-     *
-     * @param {sharedActions.SendFeedback} actionData The action data.
-     */
-    sendFeedbackError: function(actionData) {
-      this.setStoreState({
-        feedbackState: FEEDBACK_STATES.FAILED,
-        error: actionData.error
-      });
-    },
-
-    /**
-     * Resets the store to its initial state as feedback has been completed,
-     * i.e. ready for the next round of feedback.
-     */
-    feedbackComplete: function() {
-      this.resetStoreState();
-    }
-  });
-
-  return FeedbackStore;
-})();
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ /dev/null
@@ -1,323 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.shared = loop.shared || {};
-loop.shared.views = loop.shared.views || {};
-loop.shared.views.FeedbackView = (function(l10n) {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var sharedMixins = loop.shared.mixins;
-
-  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
-      loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-
-  /**
-   * Feedback outer layout.
-   *
-   * Props:
-   * -
-   */
-  var FeedbackLayout = React.createClass({displayName: "FeedbackLayout",
-    propTypes: {
-      children: React.PropTypes.element,
-      reset: React.PropTypes.func, // if not specified, no Back btn is shown
-      title: React.PropTypes.string.isRequired
-    },
-
-    render: function() {
-      var backButton = React.createElement("div", null);
-      if (this.props.reset) {
-        backButton = (
-          React.createElement("button", {className: "fx-embedded-btn-back", 
-            onClick: this.props.reset, 
-            type: "button"}, 
-            "« ", l10n.get("feedback_back_button")
-          )
-        );
-      }
-      return (
-        React.createElement("div", {className: "feedback"}, 
-          backButton, 
-          React.createElement("h3", null, this.props.title), 
-          this.props.children
-        )
-      );
-    }
-  });
-
-  /**
-   * Detailed feedback form.
-   */
-  var FeedbackForm = React.createClass({displayName: "FeedbackForm",
-    propTypes: {
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
-      pending: React.PropTypes.bool,
-      reset: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {category: "", description: ""};
-    },
-
-    getDefaultProps: function() {
-      return {pending: false};
-    },
-
-    _getCategories: function() {
-      return {
-        audio_quality: l10n.get("feedback_category_audio_quality"),
-        video_quality: l10n.get("feedback_category_video_quality"),
-        disconnected: l10n.get("feedback_category_was_disconnected"),
-        confusing: l10n.get("feedback_category_confusing2"),
-        other: l10n.get("feedback_category_other2")
-      };
-    },
-
-    _getCategoryFields: function() {
-      var categories = this._getCategories();
-      return Object.keys(categories).map(function(category, key) {
-        return (
-          React.createElement("label", {className: "feedback-category-label", key: key}, 
-            React.createElement("input", {
-              checked: this.state.category === category, 
-              className: "feedback-category-radio", 
-              name: "category", 
-              onChange: this.handleCategoryChange, 
-              ref: "category", 
-              type: "radio", 
-              value: category}), 
-            categories[category]
-          )
-        );
-      }, this);
-    },
-
-    /**
-     * Checks if the form is ready for submission:
-     *
-     * - no feedback submission should be pending.
-     * - a category (reason) must be chosen;
-     * - if the "other" category is chosen, a custom description must have been
-     *   entered by the end user;
-     *
-     * @return {Boolean}
-     */
-    _isFormReady: function() {
-      if (this.props.pending || !this.state.category) {
-        return false;
-      }
-      if (this.state.category === "other" && !this.state.description) {
-        return false;
-      }
-      return true;
-    },
-
-    handleCategoryChange: function(event) {
-      var category = event.target.value;
-      this.setState({
-        category: category
-      });
-      if (category == "other") {
-        this.refs.description.getDOMNode().focus();
-      }
-    },
-
-    handleDescriptionFieldChange: function(event) {
-      this.setState({description: event.target.value});
-    },
-
-    handleFormSubmit: function(event) {
-      event.preventDefault();
-      // XXX this feels ugly, we really want a feedbackActions object here.
-      this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
-        happy: false,
-        category: this.state.category,
-        description: this.state.description
-      }));
-    },
-
-    render: function() {
-      return (
-        React.createElement(FeedbackLayout, {
-          reset: this.props.reset, 
-          title: l10n.get("feedback_category_list_heading")}, 
-          React.createElement("form", {onSubmit: this.handleFormSubmit}, 
-            this._getCategoryFields(), 
-            React.createElement("p", null, 
-              React.createElement("input", {className: "feedback-description", 
-                name: "description", 
-                onChange: this.handleDescriptionFieldChange, 
-                placeholder: 
-                  l10n.get("feedback_custom_category_text_placeholder"), 
-                ref: "description", 
-                type: "text", 
-                value: this.state.description})
-            ), 
-            React.createElement("button", {className: "btn btn-success", 
-              disabled: !this._isFormReady(), 
-              type: "submit"}, 
-              l10n.get("feedback_submit_button")
-            )
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * Feedback received view.
-   *
-   * Props:
-   * - {Function} onAfterFeedbackReceived Function to execute after the
-   *   WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
-   */
-  var FeedbackReceived = React.createClass({displayName: "FeedbackReceived",
-    propTypes: {
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
-    },
-
-    componentDidMount: function() {
-      this._timer = setInterval(function() {
-      if (this.state.countdown == 1) {
-        clearInterval(this._timer);
-        if (this.props.onAfterFeedbackReceived) {
-          this.props.onAfterFeedbackReceived();
-        }
-        return;
-      }
-        this.setState({countdown: this.state.countdown - 1});
-      }.bind(this), 1000);
-    },
-
-    componentWillUnmount: function() {
-      if (this._timer) {
-        clearInterval(this._timer);
-      }
-    },
-
-    _renderCloseText: function() {
-      if (this.props.noCloseText) {
-        return null;
-      }
-
-      return (
-        React.createElement("p", {className: "info thank-you"}, 
-          l10n.get("feedback_window_will_close_in2", {
-            countdown: this.state.countdown,
-            num: this.state.countdown
-          }))
-      );
-    },
-
-    render: function() {
-      return (
-        React.createElement(FeedbackLayout, {title: l10n.get("feedback_thank_you_heading")}, 
-          this._renderCloseText()
-        )
-      );
-    }
-  });
-
-  /**
-   * Feedback view.
-   */
-  var FeedbackView = React.createClass({displayName: "FeedbackView",
-    mixins: [
-      Backbone.Events,
-      loop.store.StoreMixin("feedbackStore")
-    ],
-
-    propTypes: {
-      // Used by the UI showcase.
-      feedbackState: React.PropTypes.string,
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      var storeState = this.getStoreState();
-      return _.extend({}, storeState, {
-        feedbackState: this.props.feedbackState || storeState.feedbackState
-      });
-    },
-
-    reset: function() {
-      this.setState(this.getStore().getInitialStoreState());
-    },
-
-    handleHappyClick: function() {
-      // XXX: If the user is happy, we directly send this information to the
-      //      feedback API; this is a behavior we might want to revisit later.
-      this.getStore().dispatchAction(new sharedActions.SendFeedback({
-        happy: true,
-        category: "",
-        description: ""
-      }));
-    },
-
-    handleSadClick: function() {
-      this.getStore().dispatchAction(
-        new sharedActions.RequireFeedbackDetails());
-    },
-
-    _onFeedbackSent: function(err) {
-      if (err) {
-        // XXX better end user error reporting, see bug 1046738
-        console.error("Unable to send user feedback", err);
-      }
-      this.setState({pending: false, step: "finished"});
-    },
-
-    render: function() {
-      switch(this.state.feedbackState) {
-        default:
-        case FEEDBACK_STATES.INIT: {
-          return (
-            React.createElement(FeedbackLayout, {title: 
-              l10n.get("feedback_call_experience_heading2")}, 
-              React.createElement("div", {className: "faces"}, 
-                React.createElement("button", {className: "face face-happy", 
-                        onClick: this.handleHappyClick}), 
-                React.createElement("button", {className: "face face-sad", 
-                        onClick: this.handleSadClick})
-              )
-            )
-          );
-        }
-        case FEEDBACK_STATES.DETAILS: {
-          return (
-            React.createElement(FeedbackForm, {
-              feedbackStore: this.getStore(), 
-              pending: this.state.feedbackState === FEEDBACK_STATES.PENDING, 
-              reset: this.reset})
-            );
-        }
-        case FEEDBACK_STATES.PENDING:
-        case FEEDBACK_STATES.SENT:
-        case FEEDBACK_STATES.FAILED: {
-          if (this.state.error) {
-            // XXX better end user error reporting, see bug 1046738
-            console.error("Error encountered while submitting feedback",
-                          this.state.error);
-          }
-          return (
-            React.createElement(FeedbackReceived, {
-              noCloseText: this.props.noCloseText, 
-              onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
-          );
-        }
-      }
-    }
-  });
-
-  return FeedbackView;
-})(navigator.mozL10n || document.mozL10n);
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ /dev/null
@@ -1,323 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.shared = loop.shared || {};
-loop.shared.views = loop.shared.views || {};
-loop.shared.views.FeedbackView = (function(l10n) {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var sharedMixins = loop.shared.mixins;
-
-  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
-      loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-
-  /**
-   * Feedback outer layout.
-   *
-   * Props:
-   * -
-   */
-  var FeedbackLayout = React.createClass({
-    propTypes: {
-      children: React.PropTypes.element,
-      reset: React.PropTypes.func, // if not specified, no Back btn is shown
-      title: React.PropTypes.string.isRequired
-    },
-
-    render: function() {
-      var backButton = <div />;
-      if (this.props.reset) {
-        backButton = (
-          <button className="fx-embedded-btn-back"
-            onClick={this.props.reset}
-            type="button" >
-            &laquo;&nbsp;{l10n.get("feedback_back_button")}
-          </button>
-        );
-      }
-      return (
-        <div className="feedback">
-          {backButton}
-          <h3>{this.props.title}</h3>
-          {this.props.children}
-        </div>
-      );
-    }
-  });
-
-  /**
-   * Detailed feedback form.
-   */
-  var FeedbackForm = React.createClass({
-    propTypes: {
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
-      pending: React.PropTypes.bool,
-      reset: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {category: "", description: ""};
-    },
-
-    getDefaultProps: function() {
-      return {pending: false};
-    },
-
-    _getCategories: function() {
-      return {
-        audio_quality: l10n.get("feedback_category_audio_quality"),
-        video_quality: l10n.get("feedback_category_video_quality"),
-        disconnected: l10n.get("feedback_category_was_disconnected"),
-        confusing: l10n.get("feedback_category_confusing2"),
-        other: l10n.get("feedback_category_other2")
-      };
-    },
-
-    _getCategoryFields: function() {
-      var categories = this._getCategories();
-      return Object.keys(categories).map(function(category, key) {
-        return (
-          <label className="feedback-category-label" key={key}>
-            <input
-              checked={this.state.category === category}
-              className="feedback-category-radio"
-              name="category"
-              onChange={this.handleCategoryChange}
-              ref="category"
-              type="radio"
-              value={category} />
-            {categories[category]}
-          </label>
-        );
-      }, this);
-    },
-
-    /**
-     * Checks if the form is ready for submission:
-     *
-     * - no feedback submission should be pending.
-     * - a category (reason) must be chosen;
-     * - if the "other" category is chosen, a custom description must have been
-     *   entered by the end user;
-     *
-     * @return {Boolean}
-     */
-    _isFormReady: function() {
-      if (this.props.pending || !this.state.category) {
-        return false;
-      }
-      if (this.state.category === "other" && !this.state.description) {
-        return false;
-      }
-      return true;
-    },
-
-    handleCategoryChange: function(event) {
-      var category = event.target.value;
-      this.setState({
-        category: category
-      });
-      if (category == "other") {
-        this.refs.description.getDOMNode().focus();
-      }
-    },
-
-    handleDescriptionFieldChange: function(event) {
-      this.setState({description: event.target.value});
-    },
-
-    handleFormSubmit: function(event) {
-      event.preventDefault();
-      // XXX this feels ugly, we really want a feedbackActions object here.
-      this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
-        happy: false,
-        category: this.state.category,
-        description: this.state.description
-      }));
-    },
-
-    render: function() {
-      return (
-        <FeedbackLayout
-          reset={this.props.reset}
-          title={l10n.get("feedback_category_list_heading")}>
-          <form onSubmit={this.handleFormSubmit}>
-            {this._getCategoryFields()}
-            <p>
-              <input className="feedback-description"
-                name="description"
-                onChange={this.handleDescriptionFieldChange}
-                placeholder={
-                  l10n.get("feedback_custom_category_text_placeholder")}
-                ref="description"
-                type="text"
-                value={this.state.description} />
-            </p>
-            <button className="btn btn-success"
-              disabled={!this._isFormReady()}
-              type="submit">
-              {l10n.get("feedback_submit_button")}
-            </button>
-          </form>
-        </FeedbackLayout>
-      );
-    }
-  });
-
-  /**
-   * Feedback received view.
-   *
-   * Props:
-   * - {Function} onAfterFeedbackReceived Function to execute after the
-   *   WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
-   */
-  var FeedbackReceived = React.createClass({
-    propTypes: {
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
-    },
-
-    componentDidMount: function() {
-      this._timer = setInterval(function() {
-      if (this.state.countdown == 1) {
-        clearInterval(this._timer);
-        if (this.props.onAfterFeedbackReceived) {
-          this.props.onAfterFeedbackReceived();
-        }
-        return;
-      }
-        this.setState({countdown: this.state.countdown - 1});
-      }.bind(this), 1000);
-    },
-
-    componentWillUnmount: function() {
-      if (this._timer) {
-        clearInterval(this._timer);
-      }
-    },
-
-    _renderCloseText: function() {
-      if (this.props.noCloseText) {
-        return null;
-      }
-
-      return (
-        <p className="info thank-you">{
-          l10n.get("feedback_window_will_close_in2", {
-            countdown: this.state.countdown,
-            num: this.state.countdown
-          })}</p>
-      );
-    },
-
-    render: function() {
-      return (
-        <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
-          {this._renderCloseText()}
-        </FeedbackLayout>
-      );
-    }
-  });
-
-  /**
-   * Feedback view.
-   */
-  var FeedbackView = React.createClass({
-    mixins: [
-      Backbone.Events,
-      loop.store.StoreMixin("feedbackStore")
-    ],
-
-    propTypes: {
-      // Used by the UI showcase.
-      feedbackState: React.PropTypes.string,
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      var storeState = this.getStoreState();
-      return _.extend({}, storeState, {
-        feedbackState: this.props.feedbackState || storeState.feedbackState
-      });
-    },
-
-    reset: function() {
-      this.setState(this.getStore().getInitialStoreState());
-    },
-
-    handleHappyClick: function() {
-      // XXX: If the user is happy, we directly send this information to the
-      //      feedback API; this is a behavior we might want to revisit later.
-      this.getStore().dispatchAction(new sharedActions.SendFeedback({
-        happy: true,
-        category: "",
-        description: ""
-      }));
-    },
-
-    handleSadClick: function() {
-      this.getStore().dispatchAction(
-        new sharedActions.RequireFeedbackDetails());
-    },
-
-    _onFeedbackSent: function(err) {
-      if (err) {
-        // XXX better end user error reporting, see bug 1046738
-        console.error("Unable to send user feedback", err);
-      }
-      this.setState({pending: false, step: "finished"});
-    },
-
-    render: function() {
-      switch(this.state.feedbackState) {
-        default:
-        case FEEDBACK_STATES.INIT: {
-          return (
-            <FeedbackLayout title={
-              l10n.get("feedback_call_experience_heading2")}>
-              <div className="faces">
-                <button className="face face-happy"
-                        onClick={this.handleHappyClick}></button>
-                <button className="face face-sad"
-                        onClick={this.handleSadClick}></button>
-              </div>
-            </FeedbackLayout>
-          );
-        }
-        case FEEDBACK_STATES.DETAILS: {
-          return (
-            <FeedbackForm
-              feedbackStore={this.getStore()}
-              pending={this.state.feedbackState === FEEDBACK_STATES.PENDING}
-              reset={this.reset} />
-            );
-        }
-        case FEEDBACK_STATES.PENDING:
-        case FEEDBACK_STATES.SENT:
-        case FEEDBACK_STATES.FAILED: {
-          if (this.state.error) {
-            // XXX better end user error reporting, see bug 1046738
-            console.error("Error encountered while submitting feedback",
-                          this.state.error);
-          }
-          return (
-            <FeedbackReceived
-              noCloseText={this.props.noCloseText}
-              onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
-          );
-        }
-      }
-    }
-  });
-
-  return FeedbackView;
-})(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -17,17 +17,16 @@ loop.shared.mixins = (function() {
    * Sets a new root object.  This is useful for testing native DOM events so we
    * can fake them. In beforeEach(), loop.shared.mixins.setRootObject is used to
    * substitute a fake window, and in afterEach(), the real window object is
    * replaced.
    *
    * @param {Object}
    */
   function setRootObject(obj) {
-    // console.log("loop.shared.mixins: rootObject set to " + obj);
     rootObject = obj;
   }
 
   /**
    * window.location mixin. Handles changes in the call url.
    * Forces a reload of the page to ensure proper state of the webapp
    *
    * @type {Object}
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -15,29 +15,29 @@ browser.jar:
   content/browser/loop/js/conversation.js           (content/js/conversation.js)
   content/browser/loop/js/conversationAppStore.js   (content/js/conversationAppStore.js)
   content/browser/loop/js/otconfig.js               (content/js/otconfig.js)
   content/browser/loop/js/panel.js                  (content/js/panel.js)
   content/browser/loop/js/contacts.js               (content/js/contacts.js)
   content/browser/loop/js/conversationViews.js      (content/js/conversationViews.js)
   content/browser/loop/js/roomStore.js              (content/js/roomStore.js)
   content/browser/loop/js/roomViews.js              (content/js/roomViews.js)
+  content/browser/loop/js/feedbackViews.js          (content/js/feedbackViews.js)
 
   # Desktop styles
   content/browser/loop/css/contacts.css             (content/css/contacts.css)
   content/browser/loop/css/panel.css                (content/css/panel.css)
 
   # Shared styles
   content/browser/loop/shared/css/reset.css         (content/shared/css/reset.css)
   content/browser/loop/shared/css/common.css        (content/shared/css/common.css)
   content/browser/loop/shared/css/conversation.css  (content/shared/css/conversation.css)
 
   # Shared images
-  content/browser/loop/shared/img/happy.png                     (content/shared/img/happy.png)
-  content/browser/loop/shared/img/sad.png                       (content/shared/img/sad.png)
+  content/browser/loop/shared/img/helloicon.svg                 (content/shared/img/helloicon.svg)
   content/browser/loop/shared/img/icon_32.png                   (content/shared/img/icon_32.png)
   content/browser/loop/shared/img/icon_64.png                   (content/shared/img/icon_64.png)
   content/browser/loop/shared/img/spinner.svg                   (content/shared/img/spinner.svg)
   # XXX could get rid of the png spinner usages and replace them with the svg
   # one?
   content/browser/loop/shared/img/spinner.png                   (content/shared/img/spinner.png)
   content/browser/loop/shared/img/spinner@2x.png                (content/shared/img/spinner@2x.png)
   content/browser/loop/shared/img/chatbubble-arrow-left.svg     (content/shared/img/chatbubble-arrow-left.svg)
@@ -76,24 +76,21 @@ browser.jar:
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js             (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js   (content/shared/js/conversationStore.js)
   content/browser/loop/shared/js/store.js               (content/shared/js/store.js)
   content/browser/loop/shared/js/roomStates.js          (content/shared/js/roomStates.js)
   content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
   content/browser/loop/shared/js/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
-  content/browser/loop/shared/js/feedbackStore.js       (content/shared/js/feedbackStore.js)
   content/browser/loop/shared/js/dispatcher.js          (content/shared/js/dispatcher.js)
-  content/browser/loop/shared/js/feedbackApiClient.js   (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js              (content/shared/js/models.js)
   content/browser/loop/shared/js/mixins.js              (content/shared/js/mixins.js)
   content/browser/loop/shared/js/otSdkDriver.js         (content/shared/js/otSdkDriver.js)
   content/browser/loop/shared/js/views.js               (content/shared/js/views.js)
-  content/browser/loop/shared/js/feedbackViews.js       (content/shared/js/feedbackViews.js)
   content/browser/loop/shared/js/textChatStore.js       (content/shared/js/textChatStore.js)
   content/browser/loop/shared/js/textChatView.js        (content/shared/js/textChatView.js)
   content/browser/loop/shared/js/utils.js               (content/shared/js/utils.js)
   content/browser/loop/shared/js/validate.js            (content/shared/js/validate.js)
   content/browser/loop/shared/js/websocket.js           (content/shared/js/websocket.js)
 
   # Shared libs
 #ifdef DEBUG
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -332,38 +332,16 @@ p.standalone-btn-label {
 .standalone .ended-conversation {
   position: relative;
   height: 100%;
   background-color: #444;
   text-align: left; /* as backup */
   text-align: start;
 }
 
-.standalone .ended-conversation .feedback {
-  position: absolute;
-  width: 50%;
-  max-width: 400px;
-  margin: 10px auto;
-  top: 20px;
-  left: 10%;
-  right: 10%;
-  background: #FFF;
-  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
-  border-radius: 3px;
-  z-index: 1002; /* ensures the form is always on top of the control bar */
-}
-.standalone .room-conversation-wrapper .ended-conversation .feedback {
-  right: 35%;
-}
-
-html[dir="rtl"] .standalone .room-conversation-wrapper .ended-conversation .feedback {
-  right: auto;
-  left: 35%;
-}
-
 .standalone .ended-conversation .local-stream {
   /* Hide  local media stream when feedback form is shown. */
   display: none;
 }
 
 @media screen and (max-width:640px) {
   .standalone .ended-conversation .feedback {
     width: 92%;
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -129,27 +129,25 @@
     <script type="text/javascript" src="shared/libs/backbone-1.2.1.js"></script>
 
     <!-- app scripts -->
     <script type="text/javascript" src="config.js"></script>
     <script type="text/javascript" src="shared/js/utils.js"></script>
     <script type="text/javascript" src="shared/js/crypto.js"></script>
     <script type="text/javascript" src="shared/js/models.js"></script>
     <script type="text/javascript" src="shared/js/mixins.js"></script>
-    <script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="shared/js/actions.js"></script>
     <script type="text/javascript" src="shared/js/validate.js"></script>
     <script type="text/javascript" src="shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="shared/js/store.js"></script>
     <script type="text/javascript" src="shared/js/roomStates.js"></script>
     <script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
-    <script type="text/javascript" src="shared/js/feedbackStore.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
     <script type="text/javascript" src="shared/js/feedbackViews.js"></script>
     <script type="text/javascript" src="shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="shared/js/textChatView.js"></script>
     <script type="text/javascript" src="js/standaloneAppStore.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/standaloneMozLoop.js"></script>
     <script type="text/javascript" src="js/fxOSMarketplace.js"></script>
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -85,23 +85,16 @@ loop.standaloneRoomViews = (function(moz
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       failureReason: React.PropTypes.string,
       isFirefox: React.PropTypes.bool.isRequired,
       joinRoom: React.PropTypes.func.isRequired,
       roomState: React.PropTypes.string.isRequired,
       roomUsed: React.PropTypes.bool.isRequired
     },
 
-    onFeedbackSent: function() {
-      // We pass a tick to prevent React warnings regarding nested updates.
-      setTimeout(function() {
-        this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
-      }.bind(this));
-    },
-
     _renderCallToActionLink: function() {
       if (this.props.isFirefox) {
         return (
           React.createElement("a", {className: "btn btn-info", href: loop.config.learnMoreUrl}, 
             mozL10n.get("rooms_room_full_call_to_action_label", {
               clientShortname: mozL10n.get("clientShortname2")
             })
           )
@@ -113,18 +106,18 @@ loop.standaloneRoomViews = (function(moz
             brandShortname: mozL10n.get("brandShortname")
           })
         )
       );
     },
 
     render: function() {
       switch(this.props.roomState) {
+        case ROOM_STATES.ENDED:
         case ROOM_STATES.READY: {
-          // XXX: In ENDED state, we should rather display the feedback form.
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("button", {className: "btn btn-join btn-info", 
                       onClick: this.props.joinRoom}, 
                 mozL10n.get("rooms_room_join_label")
               )
             )
           );
@@ -167,32 +160,16 @@ loop.standaloneRoomViews = (function(moz
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("p", {className: "full-room-message"}, 
                 mozL10n.get("rooms_room_full_label")
               ), 
               React.createElement("p", null, this._renderCallToActionLink())
             )
           );
         }
-        case ROOM_STATES.ENDED: {
-          if (this.props.roomUsed) {
-            return (
-              React.createElement("div", {className: "ended-conversation"}, 
-                React.createElement(sharedViews.FeedbackView, {
-                  noCloseText: true, 
-                  onAfterFeedbackReceived: this.onFeedbackSent})
-              )
-            );
-          }
-
-          // In case the room was not used (no one was here), we
-          // bypass the feedback form.
-          this.onFeedbackSent();
-          return null;
-        }
         case ROOM_STATES.FAILED: {
           return (
             React.createElement(StandaloneRoomFailureView, {
               dispatcher: this.props.dispatcher, 
               failureReason: this.props.failureReason})
           );
         }
         case ROOM_STATES.INIT:
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -85,23 +85,16 @@ loop.standaloneRoomViews = (function(moz
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       failureReason: React.PropTypes.string,
       isFirefox: React.PropTypes.bool.isRequired,
       joinRoom: React.PropTypes.func.isRequired,
       roomState: React.PropTypes.string.isRequired,
       roomUsed: React.PropTypes.bool.isRequired
     },
 
-    onFeedbackSent: function() {
-      // We pass a tick to prevent React warnings regarding nested updates.
-      setTimeout(function() {
-        this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
-      }.bind(this));
-    },
-
     _renderCallToActionLink: function() {
       if (this.props.isFirefox) {
         return (
           <a className="btn btn-info" href={loop.config.learnMoreUrl}>
             {mozL10n.get("rooms_room_full_call_to_action_label", {
               clientShortname: mozL10n.get("clientShortname2")
             })}
           </a>
@@ -113,18 +106,18 @@ loop.standaloneRoomViews = (function(moz
             brandShortname: mozL10n.get("brandShortname")
           })}
         </a>
       );
     },
 
     render: function() {
       switch(this.props.roomState) {
+        case ROOM_STATES.ENDED:
         case ROOM_STATES.READY: {
-          // XXX: In ENDED state, we should rather display the feedback form.
           return (
             <div className="room-inner-info-area">
               <button className="btn btn-join btn-info"
                       onClick={this.props.joinRoom}>
                 {mozL10n.get("rooms_room_join_label")}
               </button>
             </div>
           );
@@ -167,32 +160,16 @@ loop.standaloneRoomViews = (function(moz
             <div className="room-inner-info-area">
               <p className="full-room-message">
                 {mozL10n.get("rooms_room_full_label")}
               </p>
               <p>{this._renderCallToActionLink()}</p>
             </div>
           );
         }
-        case ROOM_STATES.ENDED: {
-          if (this.props.roomUsed) {
-            return (
-              <div className="ended-conversation">
-                <sharedViews.FeedbackView
-                  noCloseText={true}
-                  onAfterFeedbackReceived={this.onFeedbackSent} />
-              </div>
-            );
-          }
-
-          // In case the room was not used (no one was here), we
-          // bypass the feedback form.
-          this.onFeedbackSent();
-          return null;
-        }
         case ROOM_STATES.FAILED: {
           return (
             <StandaloneRoomFailureView
               dispatcher={this.props.dispatcher}
               failureReason={this.props.failureReason} />
           );
         }
         case ROOM_STATES.INIT:
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -586,18 +586,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     render: function() {
       document.title = mozL10n.get("standalone_title_with_status",
                                    {clientShortname: mozL10n.get("clientShortname2"),
                                     currentStatus: mozL10n.get("status_conversation_ended")});
       return (
         React.createElement("div", {className: "ended-conversation"}, 
-          React.createElement(sharedViews.FeedbackView, {
-            onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}), 
           React.createElement(sharedViews.ConversationView, {
             audio: {enabled: false, visible: false}, 
             dispatcher: this.props.dispatcher, 
             initiate: false, 
             model: this.props.conversation, 
             sdk: this.props.sdk, 
             video: {enabled: false, visible: false}})
         )
@@ -679,17 +677,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       // Only rerender if current state has actually changed
       return nextState.callStatus !== this.state.callStatus;
     },
 
     resetCallStatus: function() {
-      this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
       return function() {
         this.setState({callStatus: "start"});
       }.bind(this);
     },
 
     /**
      * Renders the conversation views.
      */
@@ -1019,23 +1016,16 @@ loop.webapp = (function($, _, OT, mozL10
   function init() {
     var standaloneMozLoop = new loop.StandaloneMozLoop({
       baseServerUrl: loop.config.serverUrl
     });
 
     // Older non-flux based items.
     var notifications = new sharedModels.NotificationCollection();
 
-    var feedbackApiClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-        product: loop.config.feedbackProductName,
-        user_agent: navigator.userAgent,
-        url: document.location.origin
-      });
-
     // New flux items.
     var dispatcher = new loop.Dispatcher();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var sdkDriver = new loop.OTSdkDriver({
       // For the standalone, always request data channels. If they aren't
       // implemented on the client, there won't be a similar message to us, and
@@ -1062,42 +1052,31 @@ loop.webapp = (function($, _, OT, mozL10
         sdk: OT
     });
     activeRoomStore = activeRoomStore ||
       new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: standaloneMozLoop,
         sdkDriver: sdkDriver
     });
 
-    var feedbackClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-      product: loop.config.feedbackProductName,
-      user_agent: navigator.userAgent,
-      url: document.location.origin
-    });
-
     // Stores
     var standaloneAppStore = new loop.store.StandaloneAppStore({
       conversation: conversation,
       dispatcher: dispatcher,
       sdk: OT
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       activeRoomStore: activeRoomStore,
-      feedbackStore: feedbackStore,
       // This isn't used in any views, but is saved here to ensure it
       // is kept alive.
       standaloneMetricsStore: standaloneMetricsStore,
       textChatStore: textChatStore
     });
 
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -586,18 +586,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     render: function() {
       document.title = mozL10n.get("standalone_title_with_status",
                                    {clientShortname: mozL10n.get("clientShortname2"),
                                     currentStatus: mozL10n.get("status_conversation_ended")});
       return (
         <div className="ended-conversation">
-          <sharedViews.FeedbackView
-            onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
           <sharedViews.ConversationView
             audio={{enabled: false, visible: false}}
             dispatcher={this.props.dispatcher}
             initiate={false}
             model={this.props.conversation}
             sdk={this.props.sdk}
             video={{enabled: false, visible: false}} />
         </div>
@@ -679,17 +677,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       // Only rerender if current state has actually changed
       return nextState.callStatus !== this.state.callStatus;
     },
 
     resetCallStatus: function() {
-      this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
       return function() {
         this.setState({callStatus: "start"});
       }.bind(this);
     },
 
     /**
      * Renders the conversation views.
      */
@@ -1019,23 +1016,16 @@ loop.webapp = (function($, _, OT, mozL10
   function init() {
     var standaloneMozLoop = new loop.StandaloneMozLoop({
       baseServerUrl: loop.config.serverUrl
     });
 
     // Older non-flux based items.
     var notifications = new sharedModels.NotificationCollection();
 
-    var feedbackApiClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-        product: loop.config.feedbackProductName,
-        user_agent: navigator.userAgent,
-        url: document.location.origin
-      });
-
     // New flux items.
     var dispatcher = new loop.Dispatcher();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var sdkDriver = new loop.OTSdkDriver({
       // For the standalone, always request data channels. If they aren't
       // implemented on the client, there won't be a similar message to us, and
@@ -1062,42 +1052,31 @@ loop.webapp = (function($, _, OT, mozL10
         sdk: OT
     });
     activeRoomStore = activeRoomStore ||
       new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: standaloneMozLoop,
         sdkDriver: sdkDriver
     });
 
-    var feedbackClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-      product: loop.config.feedbackProductName,
-      user_agent: navigator.userAgent,
-      url: document.location.origin
-    });
-
     // Stores
     var standaloneAppStore = new loop.store.StandaloneAppStore({
       conversation: conversation,
       dispatcher: dispatcher,
       sdk: OT
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       activeRoomStore: activeRoomStore,
-      feedbackStore: feedbackStore,
       // This isn't used in any views, but is saved here to ensure it
       // is kept alive.
       standaloneMetricsStore: standaloneMetricsStore,
       textChatStore: textChatStore
     });
 
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -68,37 +68,16 @@ vendor_alttext={{vendorShortname}} logo
 ## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
 call_url_creation_date_label=(from {{call_url_creation_date}})
 call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
 call_progress_getting_media_title=Waiting for media…
 call_progress_connecting_description=Connecting…
 call_progress_ringing_description=Ringing…
 fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
 
-feedback_call_experience_heading2=How was your conversation?
-feedback_thank_you_heading=Thank you for your feedback!
-feedback_category_list_heading=What made you sad?
-feedback_category_audio_quality=Audio quality
-feedback_category_video_quality=Video quality
-feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing2=Confusing controls
-feedback_category_other2=Other
-feedback_custom_category_text_placeholder=What went wrong?
-feedback_submit_button=Submit
-feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in2):
-## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
-## In this item, don't translate the part between {{..}}
-feedback_window_will_close_in2={[ plural(countdown) ]}
-feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
-feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
-
 ## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
 ## a signed-in to signed-in user call.
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
 feedback_rejoin_button=Rejoin
 ## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
 ## an abusive user.
 feedback_report_user_button=Report User
 
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -27,58 +27,113 @@ describe("loop.store.ConversationAppStor
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
         new loop.store.ConversationAppStore({dispatcher: dispatcher});
       }).to.Throw(/mozLoop/);
     });
   });
 
   describe("#getWindowData", function() {
-    var fakeWindowData, fakeGetWindowData, fakeMozLoop, store;
+    var fakeWindowData, fakeGetWindowData, fakeMozLoop, store, getLoopPrefStub;
+    var setLoopPrefStub;
 
     beforeEach(function() {
       fakeWindowData = {
         type: "incoming",
         callId: "123456"
       };
 
       fakeGetWindowData = {
         windowId: "42"
       };
 
+      getLoopPrefStub = sandbox.stub();
+      setLoopPrefStub = sandbox.stub();
+
       fakeMozLoop = {
         getConversationWindowData: function(windowId) {
           if (windowId === "42") {
             return fakeWindowData;
           }
           return null;
-        }
+        },
+        getLoopPref: getLoopPrefStub,
+        setLoopPref: setLoopPrefStub
       };
 
       store = new loop.store.ConversationAppStore({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop
       });
     });
 
+    afterEach(function() {
+      sandbox.restore();
+    });
+
     it("should fetch the window type from the mozLoop API", function() {
       dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
 
       expect(store.getStoreState()).eql({
         windowType: "incoming"
       });
     });
 
+    it("should have the feedback period in initial state", function() {
+      getLoopPrefStub.returns(42);
+
+      // Expect ms.
+      expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
+    });
+
+    it("should have the dateLastSeen in initial state", function() {
+      getLoopPrefStub.returns(42);
+
+      // Expect ms.
+      expect(store.getInitialStoreState().feedbackTimestamp).to.eql(42 * 1000);
+    });
+
+    it("should fetch the correct pref for feedback period", function() {
+      store.getInitialStoreState();
+
+      sinon.assert.calledWithExactly(getLoopPrefStub, "feedback.periodSec");
+    });
+
+    it("should fetch the correct pref for feedback period", function() {
+      store.getInitialStoreState();
+
+      sinon.assert.calledWithExactly(getLoopPrefStub,
+                                     "feedback.dateLastSeenSec");
+    });
+
+    it("should set showFeedbackForm to true when action is triggered", function() {
+      var showFeedbackFormStub = sandbox.stub(store, "showFeedbackForm");
+
+      dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+
+      sinon.assert.calledOnce(showFeedbackFormStub);
+    });
+
+    it("should set feedback timestamp on ShowFeedbackForm action", function() {
+      var clock = sandbox.useFakeTimers();
+      // Make sure we round down the value.
+      clock.tick(1001);
+      dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+
+      sinon.assert.calledOnce(setLoopPrefStub);
+      sinon.assert.calledWithExactly(setLoopPrefStub,
+                                     "feedback.dateLastSeenSec", 1);
+    });
+
     it("should dispatch a SetupWindowData action with the data from the mozLoop API",
       function() {
         sandbox.stub(dispatcher, "dispatch");
 
         store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.SetupWindowData(_.extend({
             windowId: fakeGetWindowData.windowId
           }, fakeWindowData)));
       });
   });
-
 });
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -5,27 +5,27 @@ describe("loop.conversationViews", funct
   "use strict";
 
   var expect = chai.expect;
   var TestUtils = React.addons.TestUtils;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sandbox, view, dispatcher, contact, fakeAudioXHR, conversationStore;
-  var fakeMozLoop, fakeWindow;
+  var fakeMozLoop, fakeWindow, fakeClock;
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
-    sandbox.useFakeTimers();
+    fakeClock = sandbox.useFakeTimers();
 
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
@@ -53,16 +53,17 @@ describe("loop.conversationViews", funct
 
     fakeMozLoop = navigator.mozLoop = {
       SHARING_ROOM_URL: {
         EMAIL_FROM_CALLFAILED: 2,
         EMAIL_FROM_CONVERSATION: 3
       },
       // Dummy function, stubbed below.
       getLoopPref: function() {},
+      setLoopPref: sandbox.stub(),
       calls: {
         clearCallInProgress: sinon.stub()
       },
       composeEmail: sinon.spy(),
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
@@ -90,28 +91,24 @@ describe("loop.conversationViews", funct
       navigator: { mozLoop: fakeMozLoop },
       close: sinon.stub(),
       document: {},
       addEventListener: function() {},
       removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: {}
-    });
     conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: {},
       mozLoop: fakeMozLoop,
       sdkDriver: {}
     });
 
     loop.store.StoreMixin.register({
-      conversationStore: conversationStore,
-      feedbackStore: feedbackStore
+      conversationStore: conversationStore
     });
   });
 
   afterEach(function() {
     loop.shared.mixins.setRootObject(window);
     view = undefined;
     delete navigator.mozLoop;
     sandbox.restore();
@@ -603,30 +600,33 @@ describe("loop.conversationViews", funct
 
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
 
       expect(muteBtn.classList.contains("muted")).eql(true);
     });
   });
 
   describe("CallControllerView", function() {
-    var feedbackStore;
+    var onCallTerminatedStub;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.CallControllerView, {
           dispatcher: dispatcher,
-          mozLoop: fakeMozLoop
+          mozLoop: fakeMozLoop,
+          onCallTerminated: onCallTerminatedStub
         }));
     }
 
     beforeEach(function() {
-      feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: {}
-      });
+      onCallTerminatedStub = sandbox.stub();
+    });
+
+    afterEach(function() {
+      sandbox.restore();
     });
 
     it("should set the document title to the callerId", function() {
       conversationStore.setStoreState({
         contact: contact
       });
 
       mountTestComponent();
@@ -699,34 +699,16 @@ describe("loop.conversationViews", funct
         conversationStore.setStoreState({callState: CALL_STATES.ONGOING});
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.OngoingConversationView);
     });
 
-    it("should render the FeedbackView when the call state is 'finished'",
-      function() {
-        conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
-        view = mountTestComponent();
-
-        TestUtils.findRenderedComponentWithType(view,
-          loop.shared.views.FeedbackView);
-    });
-
-    it("should set the document title to conversation_has_ended when displaying the feedback view", function() {
-      conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
-      mountTestComponent();
-
-      expect(fakeWindow.document.title).eql("conversation_has_ended");
-    });
-
     it("should play the terminated sound when the call state is 'finished'",
       function() {
         var fakeAudio = {
           play: sinon.spy(),
           pause: sinon.spy(),
           removeAttribute: sinon.spy()
         };
         sandbox.stub(window, "Audio").returns(fakeAudio);
@@ -751,16 +733,31 @@ describe("loop.conversationViews", funct
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
 
         conversationStore.setStoreState({callState: CALL_STATES.TERMINATED});
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
+
+    it("should call onCallTerminated when the call is finished", function() {
+      conversationStore.setStoreState({
+        callState: CALL_STATES.ONGOING
+      });
+
+      view = mountTestComponent({
+        callState: CALL_STATES.FINISHED
+      });
+      // Force a state change so that it triggers componentDidUpdate.
+      view.setState({ callState: CALL_STATES.FINISHED });
+
+      sinon.assert.calledOnce(onCallTerminatedStub);
+      sinon.assert.calledWithExactly(onCallTerminatedStub);
+    });
   });
 
   describe("AcceptCallView", function() {
     var callView;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({dispatcher: dispatcher, mozLoop: fakeMozLoop}, extraProps);
       return TestUtils.renderIntoDocument(
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -1,33 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 describe("loop.conversation", function() {
   "use strict";
 
   var expect = chai.expect;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var TestUtils = React.addons.TestUtils;
-  var sharedModels = loop.shared.models,
-      fakeWindow,
-      sandbox;
+  var sharedActions = loop.shared.actions;
+  var sharedModels = loop.shared.models;
+  var fakeWindow, sandbox, getLoopPrefStub, setLoopPrefStub, mozL10nGet;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    setLoopPrefStub = sandbox.stub();
 
     navigator.mozLoop = {
       doNotDisturb: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
-      setLoopPref: sinon.stub(),
+      setLoopPref: setLoopPrefStub,
       getLoopPref: function(prefName) {
         if (prefName == "debug.sdk") {
           return false;
         }
 
         return "http://fake";
       },
       LOOP_SESSION_TYPE: {
@@ -58,17 +60,17 @@ describe("loop.conversation", function()
       document: {},
       addEventListener: function() {},
       removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
     // XXX These stubs should be hoisted in a common file
     // Bug 1040968
-    sandbox.stub(document.mozL10n, "get", function(x) {
+    mozL10nGet = sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
   afterEach(function() {
     loop.shared.mixins.setRootObject(window);
     delete navigator.mozLoop;
@@ -127,17 +129,17 @@ describe("loop.conversation", function()
         new loop.shared.actions.GetWindowData({
           windowId: "42"
         }));
     });
   });
 
   describe("AppControllerView", function() {
     var conversationStore, client, ccView, dispatcher;
-    var conversationAppStore, roomStore;
+    var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversation.AppControllerView, {
           roomStore: roomStore,
           dispatcher: dispatcher,
           mozLoop: navigator.mozLoop
         }));
@@ -210,10 +212,102 @@ describe("loop.conversation", function()
     it("should display the GenericFailureView for failures", function() {
       conversationAppStore.setStoreState({windowType: "failed"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversationViews.GenericFailureView);
     });
+
+    it("should set the correct title when rendering feedback view", function() {
+      conversationAppStore.setStoreState({showFeedbackForm: true});
+
+      ccView = mountTestComponent();
+
+      sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
+    });
+
+    it("should render FeedbackView if showFeedbackForm state is true",
+       function() {
+         conversationAppStore.setStoreState({showFeedbackForm: true});
+
+         ccView = mountTestComponent();
+
+         TestUtils.findRenderedComponentWithType(ccView, FeedbackView);
+       });
+
+    it("should dispatch a ShowFeedbackForm action if timestamp is 0",
+       function() {
+         conversationAppStore.setStoreState({feedbackTimestamp: 0});
+         sandbox.stub(dispatcher, "dispatch");
+
+         ccView = mountTestComponent();
+
+         ccView.handleCallTerminated();
+
+         sinon.assert.calledOnce(dispatcher.dispatch);
+         sinon.assert.calledWithExactly(dispatcher.dispatch,
+                                        new sharedActions.ShowFeedbackForm());
+       });
+
+    it("should set feedback timestamp if delta is > feedback period",
+       function() {
+         var feedbackTimestamp = new Date() - feedbackPeriodMs;
+         conversationAppStore.setStoreState({
+           feedbackTimestamp: feedbackTimestamp,
+           feedbackPeriod: feedbackPeriodMs
+         });
+
+         ccView = mountTestComponent();
+
+         ccView.handleCallTerminated();
+
+         sinon.assert.calledOnce(setLoopPrefStub);
+       });
+
+    it("should dispatch a ShowFeedbackForm action if delta > feedback period",
+       function() {
+         var feedbackTimestamp = new Date() - feedbackPeriodMs;
+         conversationAppStore.setStoreState({
+           feedbackTimestamp: feedbackTimestamp,
+           feedbackPeriod: feedbackPeriodMs
+         });
+         sandbox.stub(dispatcher, "dispatch");
+
+         ccView = mountTestComponent();
+
+         ccView.handleCallTerminated();
+
+         sinon.assert.calledOnce(dispatcher.dispatch);
+         sinon.assert.calledWithExactly(dispatcher.dispatch,
+                                        new sharedActions.ShowFeedbackForm());
+       });
+
+    it("should close the window if delta < feedback period", function() {
+      var feedbackTimestamp = new Date().getTime();
+      conversationAppStore.setStoreState({
+        feedbackTimestamp: feedbackTimestamp,
+        feedbackPeriod: feedbackPeriodMs
+      });
+
+      ccView = mountTestComponent();
+      var closeWindowStub = sandbox.stub(ccView, "closeWindow");
+      ccView.handleCallTerminated();
+
+      sinon.assert.calledOnce(closeWindowStub);
+    });
+
+    it("should set the correct timestamp for dateLastSeenSec", function() {
+      var feedbackTimestamp = new Date().getTime();
+      conversationAppStore.setStoreState({
+        feedbackTimestamp: feedbackTimestamp,
+        feedbackPeriod: feedbackPeriodMs
+      });
+
+      ccView = mountTestComponent();
+      var closeWindowStub = sandbox.stub(ccView, "closeWindow");
+      ccView.handleCallTerminated();
+
+      sinon.assert.calledOnce(closeWindowStub);
+    });
   });
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/desktop-local/feedbackViews_test.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+describe("loop.feedbackViews", function() {
+  "use strict";
+
+  var FeedbackView = loop.feedbackViews.FeedbackView;
+  var l10n = navigator.mozL10n || document.mozL10n;
+  var TestUtils = React.addons.TestUtils;
+  var sandbox, mozL10nGet;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    mozL10nGet = sandbox.stub(l10n, "get", function(x) {
+      return "translated:" + x;
+    });
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("FeedbackView", function() {
+    var openURLStub, getLoopPrefStub, feedbackReceivedStub;
+    var fakeURL = "fake.form", mozLoop, view;
+
+    function mountTestComponent(props) {
+      props = _.extend({
+        mozLoop: mozLoop,
+        onAfterFeedbackReceived: feedbackReceivedStub
+      }, props);
+
+      return TestUtils.renderIntoDocument(
+        React.createElement(FeedbackView, props));
+    }
+
+    beforeEach(function() {
+      openURLStub = sandbox.stub();
+      getLoopPrefStub = sandbox.stub();
+      feedbackReceivedStub = sandbox.stub();
+      mozLoop = {
+        openURL: openURLStub,
+        getLoopPref: getLoopPrefStub
+      };
+    });
+
+    afterEach(function() {
+      view = null;
+    });
+
+    it("should render a feedback view", function() {
+      view = mountTestComponent();
+
+      TestUtils.findRenderedComponentWithType(view, FeedbackView);
+    });
+
+    it("should render a button with correct text", function() {
+      view = mountTestComponent();
+
+      sinon.assert.calledWithExactly(mozL10nGet, "feedback_request_button");
+    });
+
+    it("should render a header with correct text", function() {
+      view = mountTestComponent();
+
+      sinon.assert.calledWithExactly(mozL10nGet, "feedback_window_heading");
+    });
+
+    it("should open a new page to the feedback form", function() {
+      mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
+                              .returns(fakeURL);
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+      sinon.assert.calledOnce(openURLStub);
+      sinon.assert.calledWithExactly(openURLStub, fakeURL);
+    });
+
+    it("should fetch the feedback form URL from the prefs", function() {
+      mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
+                              .returns(fakeURL);
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+      sinon.assert.calledOnce(mozLoop.getLoopPref);
+      sinon.assert.calledWithExactly(mozLoop.getLoopPref, "feedback.formURL");
+    });
+
+    it("should close the window after opening the form", function() {
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+      sinon.assert.calledOnce(feedbackReceivedStub);
+    });
+  });
+});
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -42,47 +42,46 @@
   <script>
     /*global chai,mocha */
     chai.config.includeStack = true;
     mocha.setup({ui: 'bdd', timeout: 10000});
   </script>
 
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
-  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
-  <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
-  <script src="../../content/shared/js/feedbackViews.js"></script>
   <script src="../../content/js/client.js"></script>
   <script src="../../content/js/conversationAppStore.js"></script>
   <script src="../../content/js/roomStore.js"></script>
   <script src="../../content/js/roomViews.js"></script>
   <script src="../../content/js/conversationViews.js"></script>
+  <script src="../../content/js/feedbackViews.js"></script>
   <script src="../../content/js/conversation.js"></script>
   <script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
   <script src="conversationAppStore_test.js"></script>
   <script src="client_test.js"></script>
   <script src="conversation_test.js"></script>
+  <script src="feedbackViews_test.js"></script>
   <script src="panel_test.js"></script>
   <script src="roomViews_test.js"></script>
   <script src="conversationViews_test.js"></script>
   <script src="contacts_test.js"></script>
   <script src="l10n_test.js"></script>
   <script src="roomStore_test.js"></script>
   <script>
     // Stop the default init functions running to avoid conflicts in tests
@@ -92,17 +91,17 @@
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
       it("should long only the warnings we expect", function() {
-        chai.expect(caughtWarnings.length).to.eql(30);
+        chai.expect(caughtWarnings.length).to.eql(27);
       });
     });
 
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -36,17 +36,18 @@ describe("loop.roomViews", function () {
           roomName: "fakeName",
           decryptedContext: {
             roomName: "fakeName",
             urls: []
           }
         }),
         update: sinon.stub().callsArgWith(2, null)
       },
-      telemetryAddValue: sinon.stub()
+      telemetryAddValue: sinon.stub(),
+      setLoopPref: sandbox.stub()
     };
 
     fakeWindow = {
       close: sinon.stub(),
       document: {},
       navigator: {
         mozLoop: fakeMozLoop
       },
@@ -196,18 +197,17 @@ describe("loop.roomViews", function () {
 
     describe("Copy Button", function() {
       beforeEach(function() {
         view = mountTestComponent({
           roomData: { roomUrl: "http://invalid" }
         });
       });
 
-      it("should dispatch a CopyRoomUrl action when the copy button is " +
-        "pressed", function() {
+      it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
           var copyBtn = view.getDOMNode().querySelector(".btn-copy");
 
           React.addons.TestUtils.Simulate.click(copyBtn);
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
             roomUrl: "http://invalid",
             from: "conversation"
@@ -285,40 +285,38 @@ describe("loop.roomViews", function () {
         });
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
     });
   });
 
   describe("DesktopRoomConversationView", function() {
-    var view;
+    var view, onCallTerminatedStub;
 
     beforeEach(function() {
-      loop.store.StoreMixin.register({
-        feedbackStore: new loop.store.FeedbackStore(dispatcher, {
-          feedbackClient: {}
-        })
-      });
       sandbox.stub(dispatcher, "dispatch");
       fakeMozLoop.getLoopPref = function(prefName) {
         if (prefName == "contextInConversations.enabled") {
           return true;
         }
         return "test";
       };
+      onCallTerminatedStub = sandbox.stub();
     });
 
-    function mountTestComponent() {
+    function mountTestComponent(props) {
+      props = _.extend({
+        dispatcher: dispatcher,
+        roomStore: roomStore,
+        mozLoop: fakeMozLoop,
+        onCallTerminated: onCallTerminatedStub
+      }, props);
       return TestUtils.renderIntoDocument(
-        React.createElement(loop.roomViews.DesktopRoomConversationView, {
-          dispatcher: dispatcher,
-          roomStore: roomStore,
-          mozLoop: fakeMozLoop
-        }));
+        React.createElement(loop.roomViews.DesktopRoomConversationView, props));
     }
 
     it("should dispatch a setMute action when the audio mute button is pressed",
       function() {
         view = mountTestComponent();
 
         view.setState({audioMuted: true});
 
@@ -367,18 +365,17 @@ describe("loop.roomViews", function () {
 
       view.setState({audioMuted: true});
 
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
 
       expect(muteBtn.classList.contains("muted")).eql(true);
     });
 
-    it("should dispatch a `StartScreenShare` action when sharing is not active " +
-       "and the screen share button is pressed", function() {
+    it("should dispatch a `StartScreenShare` action when sharing is not active and the screen share button is pressed", function() {
       view = mountTestComponent();
 
       view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
 
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
 
       React.addons.TestUtils.Simulate.click(muteBtn);
 
@@ -414,28 +411,26 @@ describe("loop.roomViews", function () {
 
     describe("#componentWillUpdate", function() {
       function expectActionDispatched(component) {
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           sinon.match.instanceOf(sharedActions.SetupStreamElements));
       }
 
-      it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
-        "is entered", function() {
+      it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
           var component = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
           expectActionDispatched(component);
         });
 
-      it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
-        "re-entered", function() {
+      it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is re-entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
           var component = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
           expectActionDispatched(component);
         });
     });
@@ -484,28 +479,28 @@ describe("loop.roomViews", function () {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
             loop.roomViews.DesktopRoomConversationView);
         });
 
-      it("should render the FeedbackView if roomState is `ENDED`",
-        function() {
-          activeRoomStore.setStoreState({
-            roomState: ROOM_STATES.ENDED,
-            used: true
-          });
+      it("should call onCallTerminated when the call ended", function() {
+        activeRoomStore.setStoreState({
+          roomState: ROOM_STATES.ENDED,
+          used: true
+        });
 
-          view = mountTestComponent();
+        view = mountTestComponent();
+        // Force a state change so that it triggers componentDidUpdate
+        view.setState({ foo: "bar" });
 
-          TestUtils.findRenderedComponentWithType(view,
-            loop.shared.views.FeedbackView);
-        });
+        sinon.assert.calledOnce(onCallTerminatedStub);
+      });
 
       it("should display loading spinner when localSrcVideoObject is null",
          function() {
            activeRoomStore.setStoreState({
              roomState: ROOM_STATES.MEDIA_WAIT,
              localSrcVideoObject: null
            });
 
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -147,26 +147,22 @@ class Test1BrowserCall(MarionetteTestCas
         button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
 
         button.click()
 
     def standalone_check_remote_screenshare(self):
         self.switch_to_standalone()
         self.check_video(".screen-share-video")
 
-    def remote_leave_room_and_verify_feedback(self):
+    def remote_leave_room(self):
         self.switch_to_standalone()
         button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
 
         button.click()
 
-        # check that the feedback form is displayed
-        feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
-        self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
-
         self.switch_to_chatbox()
         # check that the local view reverts to the preview mode
         self.wait_for_element_displayed(By.CLASS_NAME, "room-invitation-content")
 
     def local_get_chatbox_window_expr(self, expr):
         """
         :expr: a sub-expression which must begin with a property of the
         global content window (e.g. "location.path")
@@ -255,15 +251,15 @@ class Test1BrowserCall(MarionetteTestCas
         # self.local_enable_screenshare()
         # self.standalone_check_remote_screenshare()
 
         # We hangup on the remote (standalone) side, because this also leaves
         # the local chatbox with the local publishing media still connected,
         # which means that the local_check_connection_length below
         # verifies that the connection is noted at the time the remote media
         # drops, rather than waiting until the window closes.
-        self.remote_leave_room_and_verify_feedback()
+        self.remote_leave_room()
 
         self.local_check_connection_length_noted()
 
     def tearDown(self):
         self.loop_test_servers.shutdown()
         MarionetteTestCase.tearDown(self)
--- a/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
@@ -10,14 +10,14 @@
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 add_task(loadLoopPanel);
 
 add_task(function* test_mozLoop_pluralStrings() {
   Assert.ok(gMozLoopAPI, "mozLoop should exist");
 
-  var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
-  Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
-               "This window will close in {{countdown}} seconds");
+  var strings = JSON.parse(gMozLoopAPI.getStrings("import_contacts_success_message"));
   Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
-               "This window will close in {{countdown}} second");
+               "{{total}} contact was successfully imported.");
+  Assert.equal(gMozLoopAPI.getPluralForm(3, strings.textContent),
+               "{{total}} contacts were successfully imported.");
 });
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -561,40 +561,16 @@ describe("loop.store.ActiveRoomStore", f
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo(expectedData));
       });
     });
   });
 
-  describe("#feedbackComplete", function() {
-    it("should set the room state to READY", function() {
-      store.setStoreState({
-        roomState: ROOM_STATES.ENDED,
-        used: true
-      });
-
-      store.feedbackComplete(new sharedActions.FeedbackComplete());
-
-      expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
-    });
-
-    it("should reset the 'used' state", function() {
-      store.setStoreState({
-        roomState: ROOM_STATES.ENDED,
-        used: true
-      });
-
-      store.feedbackComplete(new sharedActions.FeedbackComplete());
-
-      expect(store.getStoreState().used).eql(false);
-    });
-  });
-
   describe("#videoDimensionsChanged", function() {
     it("should not contain any video dimensions at the very start", function() {
       expect(store.getStoreState()).eql(store.getInitialStoreState());
     });
 
     it("should update the store with new video dimensions", function() {
       var actionData = {
         isLocal: true,
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackApiClient_test.js
+++ /dev/null
@@ -1,184 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-describe("loop.FeedbackAPIClient", function() {
-  "use strict";
-
-  var expect = chai.expect;
-  var sandbox,
-      fakeXHR,
-      requests = [];
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-    fakeXHR = sandbox.useFakeXMLHttpRequest();
-    requests = [];
-    // https://github.com/cjohansen/Sinon.JS/issues/393
-    fakeXHR.xhr.onCreate = function (xhr) {
-      requests.push(xhr);
-    };
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("#constructor", function() {
-    it("should require a baseUrl setting", function() {
-      expect(function() {
-        return new loop.FeedbackAPIClient();
-      }).to.Throw(/required 'baseUrl'/);
-    });
-
-    it("should require a product setting", function() {
-      expect(function() {
-        return new loop.FeedbackAPIClient("http://fake", {});
-      }).to.Throw(/required 'product'/);
-    });
-  });
-
-  describe("constructed", function() {
-    var client;
-
-    beforeEach(function() {
-      client = new loop.FeedbackAPIClient("http://fake/feedback", {
-        product: "Hello",
-        version: "42b1"
-      });
-    });
-
-    describe("#send", function() {
-      it("should send happy feedback data", function() {
-        var feedbackData = {
-          happy: true,
-          description: "Happy User"
-        };
-
-        client.send(feedbackData, function(){});
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).to.be.equal("http://fake/feedback");
-        expect(requests[0].method).to.be.equal("POST");
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.happy).eql(true);
-        expect(parsed.description).eql("Happy User");
-      });
-
-      it("should send sad feedback data", function() {
-        var feedbackData = {
-          happy: false,
-          category: "confusing"
-        };
-
-        client.send(feedbackData, function(){});
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).to.be.equal("http://fake/feedback");
-        expect(requests[0].method).to.be.equal("POST");
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.happy).eql(false);
-        expect(parsed.product).eql("Hello");
-        expect(parsed.category).eql("confusing");
-        expect(parsed.description).eql("Sad User");
-      });
-
-      it("should send formatted feedback data", function() {
-        client.send({
-          happy: false,
-          category: "other",
-          description: "it's far too awesome!"
-        }, function(){});
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).eql("http://fake/feedback");
-        expect(requests[0].method).eql("POST");
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.happy).eql(false);
-        expect(parsed.product).eql("Hello");
-        expect(parsed.category).eql("other");
-        expect(parsed.description).eql("it's far too awesome!");
-      });
-
-      it("should send product information", function() {
-        client.send({product: "Hello"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.product).eql("Hello");
-      });
-
-      it("should send platform information when provided", function() {
-        client.send({platform: "Windows 8"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.platform).eql("Windows 8");
-      });
-
-      it("should send channel information when provided", function() {
-        client.send({channel: "beta"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.channel).eql("beta");
-      });
-
-      it("should send version information when provided", function() {
-        client.send({version: "42b1"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.version).eql("42b1");
-      });
-
-      it("should send user_agent information when provided", function() {
-        client.send({user_agent: "MOZAGENT"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.user_agent).eql("MOZAGENT");
-      });
-
-      it("should send url information when provided", function() {
-        client.send({url: "http://fake.invalid"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.url).eql("http://fake.invalid");
-      });
-
-      it("should throw on invalid feedback data", function() {
-        expect(function() {
-          client.send("invalid data", function(){});
-        }).to.Throw(/Invalid/);
-      });
-
-      it("should throw on unsupported field name", function() {
-        expect(function() {
-          client.send({bleh: "bah"}, function(){});
-        }).to.Throw(/Unsupported/);
-      });
-
-      it("should call passed callback on success", function() {
-        var cb = sandbox.spy();
-        var fakeResponseData = {description: "confusing"};
-        client.send({category: "confusing"}, cb);
-
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            JSON.stringify(fakeResponseData));
-
-        sinon.assert.calledOnce(cb);
-        sinon.assert.calledWithExactly(cb, null, fakeResponseData);
-      });
-
-      it("should call passed callback on error", function() {
-        var cb = sandbox.spy();
-        var fakeErrorData = {error: true};
-        client.send({category: "confusing"}, cb);
-
-        requests[0].respond(400, {"Content-Type": "application/json"},
-                            JSON.stringify(fakeErrorData));
-
-        sinon.assert.calledOnce(cb);
-        sinon.assert.calledWithExactly(cb, sinon.match(function(err) {
-          return /Bad Request/.test(err);
-        }));
-      });
-    });
-  });
-});
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackStore_test.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-describe("loop.store.FeedbackStore", function () {
-  "use strict";
-
-  var expect = chai.expect;
-  var sharedActions = loop.shared.actions;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-  var sandbox, dispatcher, store, feedbackClient;
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-
-    dispatcher = new loop.Dispatcher();
-
-    feedbackClient = new loop.FeedbackAPIClient("http://invalid", {
-      product: "Loop"
-    });
-
-    store = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("#constructor", function() {
-    it("should throw an error if feedbackClient is missing", function() {
-      expect(function() {
-        new loop.store.FeedbackStore(dispatcher);
-      }).to.Throw(/feedbackClient/);
-    });
-
-    it("should set the store to the INIT feedback state", function() {
-      var fakeStore = new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: feedbackClient
-      });
-
-      expect(fakeStore.getStoreState("feedbackState"))
-          .eql(FEEDBACK_STATES.INIT);
-    });
-  });
-
-  describe("#requireFeedbackDetails", function() {
-    it("should transition to DETAILS state", function() {
-      store.requireFeedbackDetails(new sharedActions.RequireFeedbackDetails());
-
-      expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.DETAILS);
-    });
-  });
-
-  describe("#sendFeedback", function() {
-    var sadFeedbackData = {
-      happy: false,
-      category: "fakeCategory",
-      description: "fakeDescription"
-    };
-
-    beforeEach(function() {
-      store.requireFeedbackDetails();
-    });
-
-    it("should send feedback data over the feedback client", function() {
-      sandbox.stub(feedbackClient, "send");
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-
-      sinon.assert.calledOnce(feedbackClient.send);
-      sinon.assert.calledWithMatch(feedbackClient.send, sadFeedbackData);
-    });
-
-    it("should transition to PENDING state", function() {
-      sandbox.stub(feedbackClient, "send");
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-
-      expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.PENDING);
-    });
-
-    it("should transition to SENT state on successful submission", function(done) {
-      sandbox.stub(feedbackClient, "send", function(data, cb) {
-        cb(null);
-      });
-
-      store.once("change:feedbackState", function() {
-        expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.SENT);
-        done();
-      });
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-    });
-
-    it("should transition to FAILED state on failed submission", function(done) {
-      sandbox.stub(feedbackClient, "send", function(data, cb) {
-        cb(new Error("failed"));
-      });
-
-      store.once("change:feedbackState", function() {
-        expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.FAILED);
-        done();
-      });
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-    });
-  });
-
-  describe("feedbackComplete", function() {
-    it("should reset the store state", function() {
-      store.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
-      store.feedbackComplete();
-
-      expect(store.getStoreState()).eql({
-        feedbackState: FEEDBACK_STATES.INIT
-      });
-    });
-  });
-});
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackViews_test.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-describe("loop.shared.views.FeedbackView", function() {
-  "use strict";
-
-  var expect = chai.expect;
-  var l10n = navigator.mozL10n || document.mozL10n;
-  var TestUtils = React.addons.TestUtils;
-  var sharedActions = loop.shared.actions;
-  var sharedViews = loop.shared.views;
-
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-  var sandbox, comp, dispatcher, fakeFeedbackClient, feedbackStore;
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-    dispatcher = new loop.Dispatcher();
-    fakeFeedbackClient = {send: sandbox.stub()};
-    feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: fakeFeedbackClient
-    });
-    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
-    comp = TestUtils.renderIntoDocument(
-      React.createElement(sharedViews.FeedbackView));
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  // local test helpers
-  function clickHappyFace(component) {
-    var happyFace = component.getDOMNode().querySelector(".face-happy");
-    TestUtils.Simulate.click(happyFace);
-  }
-
-  function clickSadFace(component) {
-    var sadFace = component.getDOMNode().querySelector(".face-sad");
-    TestUtils.Simulate.click(sadFace);
-  }
-
-  function fillSadFeedbackForm(component, category, text) {
-    TestUtils.Simulate.change(
-      component.getDOMNode().querySelector("[value='" + category + "']"));
-
-    if (text) {
-      TestUtils.Simulate.change(
-        component.getDOMNode().querySelector("[name='description']"), {
-          target: {value: "fake reason"}
-        });
-    }
-  }
-
-  function submitSadFeedbackForm(component, category, text) {
-    TestUtils.Simulate.submit(component.getDOMNode().querySelector("form"));
-  }
-
-  describe("Happy feedback", function() {
-    it("should dispatch a SendFeedback action", function() {
-      var dispatch = sandbox.stub(dispatcher, "dispatch");
-
-      clickHappyFace(comp);
-
-      sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
-        happy: true,
-        category: "",
-        description: ""
-      }));
-    });
-
-    it("should thank the user once feedback data is sent", function() {
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
-      expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
-      expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
-        .eql(null);
-    });
-
-    it("should not display the countdown text if noCloseText is true", function() {
-      comp = TestUtils.renderIntoDocument(
-        React.createElement(sharedViews.FeedbackView, {
-          noCloseText: true
-        }));
-
-      expect(comp.getDOMNode().querySelector(".info.thank-you")).eql(null);
-    });
-  });
-
-  describe("Sad feedback", function() {
-    it("should bring the user to feedback form when clicking on the sad face",
-      function() {
-        clickSadFace(comp);
-
-        expect(comp.getDOMNode().querySelectorAll("form")).not.eql(null);
-      });
-
-    it("should render a back button", function() {
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-
-      expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
-        .not.eql(null);
-    });
-
-    it("should reset the view when clicking the back button", function() {
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-
-      TestUtils.Simulate.click(
-        comp.getDOMNode().querySelector("button.fx-embedded-btn-back"));
-
-      expect(comp.getDOMNode().querySelector(".faces")).not.eql(null);
-    });
-
-    it("should disable the form submit button when no category is chosen",
-      function() {
-        clickSadFace(comp);
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
-      });
-
-    it("should disable the form submit button when the 'other' category is " +
-       "chosen but no description has been entered yet",
-      function() {
-        clickSadFace(comp);
-        fillSadFeedbackForm(comp, "other");
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
-      });
-
-    it("should enable the form submit button when the 'other' category is " +
-       "chosen and a description is entered",
-      function() {
-        clickSadFace(comp);
-        fillSadFeedbackForm(comp, "other", "fake");
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
-      });
-
-    it("should enable the form submit button once a predefined category is " +
-       "chosen",
-      function() {
-        clickSadFace(comp);
-
-        fillSadFeedbackForm(comp, "confusing");
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
-      });
-
-    it("should send feedback data when the form is submitted", function() {
-      var dispatch = sandbox.stub(dispatcher, "dispatch");
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-      fillSadFeedbackForm(comp, "confusing");
-
-      submitSadFeedbackForm(comp);
-
-      sinon.assert.calledOnce(dispatch);
-      sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
-        happy: false,
-        category: "confusing",
-        description: ""
-      }));
-    });
-
-    it("should send feedback data when user has entered a custom description",
-      function() {
-        clickSadFace(comp);
-
-        fillSadFeedbackForm(comp, "other", "fake reason");
-        submitSadFeedbackForm(comp);
-
-        sinon.assert.calledOnce(fakeFeedbackClient.send);
-        sinon.assert.calledWith(fakeFeedbackClient.send, {
-          happy: false,
-          category: "other",
-          description: "fake reason"
-        });
-      });
-
-    it("should thank the user when feedback data has been sent", function() {
-      fakeFeedbackClient.send = function(data, cb) {
-        cb();
-      };
-      clickSadFace(comp);
-      fillSadFeedbackForm(comp, "confusing");
-      submitSadFeedbackForm(comp);
-
-      expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
-    });
-  });
-});
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -47,47 +47,41 @@
   </script>
 
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/crypto.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
-  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
-  <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
-  <script src="../../content/shared/js/feedbackViews.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="mixins_test.js"></script>
   <script src="utils_test.js"></script>
   <script src="crypto_test.js"></script>
   <script src="views_test.js"></script>
   <script src="websocket_test.js"></script>
-  <script src="feedbackApiClient_test.js"></script>
-  <script src="feedbackViews_test.js"></script>
   <script src="validate_test.js"></script>
   <script src="dispatcher_test.js"></script>
   <script src="activeRoomStore_test.js"></script>
   <script src="fxOSActiveRoomStore_test.js"></script>
   <script src="conversationStore_test.js"></script>
-  <script src="feedbackStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
   <script src="store_test.js"></script>
   <script src="textChatStore_test.js"></script>
   <script src="textChatView_test.js"></script>
   <script>
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -44,29 +44,26 @@
     chai.config.includeStack = true;
     mocha.setup({ui: 'bdd', timeout: 10000});
   </script>
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
-  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
-  <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
-  <script src="../../content/shared/js/feedbackViews.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../standalone/content/js/multiplexGum.js"></script>
   <script src="../../standalone/content/js/standaloneAppStore.js"></script>
   <script src="../../standalone/content/js/standaloneClient.js"></script>
   <script src="../../standalone/content/js/standaloneMozLoop.js"></script>
   <script src="../../standalone/content/js/fxOSMarketplace.js"></script>
   <script src="../../standalone/content/js/standaloneRoomViews.js"></script>
   <script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
@@ -83,17 +80,17 @@
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
       it("should long only the warnings we expect", function() {
-        chai.expect(caughtWarnings.length).to.eql(15);
+        chai.expect(caughtWarnings.length).to.eql(11);
       });
     });
 
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
 </script>
 </body>
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -10,35 +10,31 @@ describe("loop.standaloneRoomViews", fun
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
 
-  var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
+  var sandbox, dispatcher, activeRoomStore, dispatch;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     dispatch = sandbox.stub(dispatcher, "dispatch");
     activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: {},
       sdkDriver: {}
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: {}
     });
-    feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: {}
-    });
     loop.store.StoreMixin.register({
       activeRoomStore: activeRoomStore,
-      feedbackStore: feedbackStore,
       textChatStore: textChatStore
     });
 
     sandbox.useFakeTimers();
 
     // Prevents audio request errors in the test console.
     sandbox.useFakeXMLHttpRequest();
   });
@@ -496,48 +492,16 @@ describe("loop.standaloneRoomViews", fun
 
           TestUtils.Simulate.click(getLeaveButton(view));
 
           sinon.assert.calledOnce(dispatch);
           sinon.assert.calledWithExactly(dispatch, new sharedActions.LeaveRoom());
         });
       });
 
-      describe("Feedback", function() {
-        beforeEach(function() {
-          activeRoomStore.setStoreState({
-            roomState: ROOM_STATES.ENDED,
-            used: true
-          });
-        });
-
-        it("should display a feedback form when the user leaves the room",
-          function() {
-            expect(view.getDOMNode().querySelector(".faces")).not.eql(null);
-          });
-
-        it("should dispatch a `FeedbackComplete` action after feedback is sent",
-          function() {
-            feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
-            sandbox.clock.tick(
-              loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS * 1000 + 1000);
-
-            sinon.assert.calledOnce(dispatch);
-            sinon.assert.calledWithExactly(dispatch, new sharedActions.FeedbackComplete());
-          });
-
-        it("should NOT display a feedback form if the room has not been used",
-          function() {
-            activeRoomStore.setStoreState({used: false});
-            expect(view.getDOMNode().querySelector(".faces")).eql(null);
-          });
-
-      });
-
       describe("Mute", function() {
         it("should render a local avatar if video is muted",
           function() {
             activeRoomStore.setStoreState({
               roomState: ROOM_STATES.SESSION_CONNECTED,
               videoMuted: true
             });
 
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -18,21 +18,16 @@ describe("loop.webapp", function() {
       fakeAudioXHR,
       dispatcher,
       WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     notifications = new sharedModels.NotificationCollection();
-    loop.store.StoreMixin.register({
-      feedbackStore: new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: {}
-      })
-    });
 
     stubGetPermsAndCacheMedia = sandbox.stub(
       loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
 
     fakeAudioXHR = {
       open: sinon.spy(),
       send: function() {},
       abort: function() {},
@@ -49,17 +44,16 @@ describe("loop.webapp", function() {
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#init", function() {
     beforeEach(function() {
       sandbox.stub(React, "render");
-      loop.config.feedbackApiUrl = "http://fake.invalid";
       sandbox.stub(loop.Dispatcher.prototype, "dispatch");
     });
 
     it("should create the WebappRootView", function() {
       loop.webapp.init();
 
       sinon.assert.calledOnce(React.render);
       sinon.assert.calledWith(React.render,
@@ -1072,20 +1066,16 @@ describe("loop.webapp", function() {
             sdk: {},
             onAfterFeedbackReceived: function(){}
           }));
     });
 
     it("should render a ConversationView", function() {
       TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
     });
-
-    it("should render a FeedbackView", function() {
-      TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
-    });
   });
 
   describe("PromoteFirefoxView", function() {
     describe("#render", function() {
       it("should not render when using Firefox", function() {
         var comp = TestUtils.renderIntoDocument(
           React.createElement(loop.webapp.PromoteFirefoxView, {
             isFirefox: true
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -37,33 +37,31 @@
       window.OTProperties.configURL = window.OTProperties.assetURL + 'js/dynamic_config.min.js';
     </script>
     <script src="../content/js/multiplexGum.js"></script>
     <script src="../content/shared/libs/sdk.js"></script>
     <script src="../content/shared/libs/react-0.12.2.js"></script>
     <script src="../content/shared/libs/jquery-2.1.4.js"></script>
     <script src="../content/shared/libs/lodash-3.9.3.js"></script>
     <script src="../content/shared/libs/backbone-1.2.1.js"></script>
-    <script src="../content/shared/js/feedbackApiClient.js"></script>
     <script src="../content/shared/js/actions.js"></script>
     <script src="../content/shared/js/utils.js"></script>
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/websocket.js"></script>
     <script src="../content/shared/js/validate.js"></script>
     <script src="../content/shared/js/dispatcher.js"></script>
     <script src="../content/shared/js/store.js"></script>
     <script src="../content/shared/js/conversationStore.js"></script>
     <script src="../content/shared/js/roomStates.js"></script>
     <script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
     <script src="../content/shared/js/activeRoomStore.js"></script>
-    <script src="../content/shared/js/feedbackStore.js"></script>
     <script src="../content/shared/js/views.js"></script>
-    <script src="../content/shared/js/feedbackViews.js"></script>
     <script src="../content/shared/js/textChatStore.js"></script>
+    <script src="../content/js/feedbackViews.js"></script>
     <script src="../content/shared/js/textChatView.js"></script>
     <script src="../content/js/roomStore.js"></script>
     <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../standalone/content/js/multiplexGum.js"></script>
     <script src="../standalone/content/js/webapp.js"></script>
     <script src="../standalone/content/js/standaloneRoomViews.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -27,23 +27,22 @@
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
-  var FeedbackView = loop.shared.views.FeedbackView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
   var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
@@ -71,24 +70,16 @@
     }
     window.removeEventListener(eventName, func);
   };
 
   loop.shared.mixins.setRootObject(rootObject);
 
   var dispatcher = new loop.Dispatcher();
 
-  // Feedback API client configured to send data to the stage input server,
-  // which is available at https://input.allizom.org
-  var stageFeedbackApiClient = new loop.FeedbackAPIClient(
-    "https://input.allizom.org/api/v1/feedback", {
-      product: "Loop"
-    }
-  );
-
   var mockSDK = _.extend({
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
         message: message.message
       }));
     }
   }, Backbone.Events);
 
@@ -276,19 +267,16 @@
     remoteVideoEnabled: false,
     mediaConnected: true
   });
   var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
-  var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-    feedbackClient: stageFeedbackApiClient
-  });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
     sdkDriver: mockSDK
   });
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
@@ -349,17 +337,16 @@
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Cool",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
-    feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
@@ -856,34 +843,23 @@
                   remoteVideoEnabled: false, 
                   video: {enabled: true}})
               )
             )
 
           ), 
 
           React.createElement(Section, {name: "FeedbackView"}, 
-            React.createElement("p", {className: "note"}, 
-              React.createElement("strong", null, "Note:"), " For the useable demo, you can access submitted data at ", 
-              React.createElement("a", {href: "https://input.allizom.org/"}, "input.allizom.org"), "."
+            React.createElement("p", {className: "note"}
             ), 
             React.createElement(Example, {dashed: true, 
                      style: {width: "300px", height: "272px"}, 
                      summary: "Default (useable demo)"}, 
-              React.createElement(FeedbackView, {feedbackStore: feedbackStore})
-            ), 
-            React.createElement(Example, {dashed: true, 
-                     style: {width: "300px", height: "272px"}, 
-                     summary: "Detailed form"}, 
-              React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.DETAILS, feedbackStore: feedbackStore})
-            ), 
-            React.createElement(Example, {dashed: true, 
-                     style: {width: "300px", height: "272px"}, 
-                     summary: "Thank you!"}, 
-              React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.SENT, feedbackStore: feedbackStore})
+              React.createElement(FeedbackView, {mozLoop: {}, 
+                            onAfterFeedbackReceived: function() {}})
             )
           ), 
 
           React.createElement(Section, {name: "AlertMessages"}, 
             React.createElement(Example, {summary: "Various alerts"}, 
               React.createElement("div", {className: "alert alert-warning"}, 
                 React.createElement("button", {className: "close"}), 
                 React.createElement("p", {className: "message"}, 
@@ -921,16 +897,17 @@
               height: 254, 
               summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)", 
               width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   roomState: ROOM_STATES.INIT, 
                   roomStore: invitationRoomStore})
               )
             ), 
 
             React.createElement(FramedExample, {
               dashed: true, 
               height: 394, 
@@ -938,56 +915,60 @@
               width: 298}, 
               /* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */
               React.createElement("div", {className: "fx-embedded overflow-hidden"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: desktopRoomStoreLoading})
               )
             ), 
 
             React.createElement(FramedExample, {height: 254, 
                            summary: "Desktop room conversation"}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: roomStore})
               )
             ), 
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            summary: "Desktop room conversation local face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomStore: desktopLocalFaceMuteRoomStore})
               )
             ), 
 
             React.createElement(FramedExample, {dashed: true, height: 394, 
                            summary: "Desktop room conversation remote face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   roomStore: desktopRemoteFaceMuteRoomStore})
               )
             )
           ), 
 
           React.createElement(Section, {name: "StandaloneRoomView"}, 
             React.createElement(FramedExample, {cssClass: "standalone", 
                            dashed: true, 
@@ -1169,30 +1150,16 @@
                   dispatcher: dispatcher, 
                   isFirefox: false})
               )
             ), 
 
             React.createElement(FramedExample, {cssClass: "standalone", 
                            dashed: true, 
                            height: 483, 
-                           summary: "Standalone room conversation (feedback)", 
-                           width: 644}, 
-              React.createElement("div", {className: "standalone"}, 
-                React.createElement(StandaloneRoomView, {
-                  activeRoomStore: endedRoomStore, 
-                  dispatcher: dispatcher, 
-                  feedbackStore: feedbackStore, 
-                  isFirefox: false})
-              )
-            ), 
-
-            React.createElement(FramedExample, {cssClass: "standalone", 
-                           dashed: true, 
-                           height: 483, 
                            summary: "Standalone room conversation (failed)", 
                            width: 644}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   activeRoomStore: failedRoomStore, 
                   dispatcher: dispatcher, 
                   isFirefox: false})
               )
@@ -1310,17 +1277,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 24;
+      var expectedWarningsCount = 23;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       if (uncaughtError || warningsMismatch) {
         $("#results").append("<div class='failures'><em>" +
           (!!(uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
         if (warningsMismatch) {
           $("#results").append("<li class='test fail'>" +
             "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
             "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -27,23 +27,22 @@
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
-  var FeedbackView = loop.shared.views.FeedbackView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
   var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
@@ -71,24 +70,16 @@
     }
     window.removeEventListener(eventName, func);
   };
 
   loop.shared.mixins.setRootObject(rootObject);
 
   var dispatcher = new loop.Dispatcher();
 
-  // Feedback API client configured to send data to the stage input server,
-  // which is available at https://input.allizom.org
-  var stageFeedbackApiClient = new loop.FeedbackAPIClient(
-    "https://input.allizom.org/api/v1/feedback", {
-      product: "Loop"
-    }
-  );
-
   var mockSDK = _.extend({
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
         message: message.message
       }));
     }
   }, Backbone.Events);
 
@@ -276,19 +267,16 @@
     remoteVideoEnabled: false,
     mediaConnected: true
   });
   var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
-  var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-    feedbackClient: stageFeedbackApiClient
-  });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
     sdkDriver: mockSDK
   });
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
@@ -349,17 +337,16 @@
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Cool",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
-    feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
@@ -857,33 +844,22 @@
                   video={{enabled: true}} />
               </div>
             </FramedExample>
 
           </Section>
 
           <Section name="FeedbackView">
             <p className="note">
-              <strong>Note:</strong> For the useable demo, you can access submitted data at&nbsp;
-              <a href="https://input.allizom.org/">input.allizom.org</a>.
             </p>
             <Example dashed={true}
                      style={{width: "300px", height: "272px"}}
                      summary="Default (useable demo)">
-              <FeedbackView feedbackStore={feedbackStore} />
-            </Example>
-            <Example dashed={true}
-                     style={{width: "300px", height: "272px"}}
-                     summary="Detailed form">
-              <FeedbackView feedbackState={FEEDBACK_STATES.DETAILS} feedbackStore={feedbackStore} />
-            </Example>
-            <Example dashed={true}
-                     style={{width: "300px", height: "272px"}}
-                     summary="Thank you!">
-              <FeedbackView feedbackState={FEEDBACK_STATES.SENT} feedbackStore={feedbackStore}/>
+              <FeedbackView mozLoop={{}}
+                            onAfterFeedbackReceived={function() {}} />
             </Example>
           </Section>
 
           <Section name="AlertMessages">
             <Example summary="Various alerts">
               <div className="alert alert-warning">
                 <button className="close"></button>
                 <p className="message">
@@ -921,16 +897,17 @@
               height={254}
               summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
               width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   roomState={ROOM_STATES.INIT}
                   roomStore={invitationRoomStore} />
               </div>
             </FramedExample>
 
             <FramedExample
               dashed={true}
               height={394}
@@ -938,56 +915,60 @@
               width={298}>
               {/* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */}
               <div className="fx-embedded overflow-hidden">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={desktopRoomStoreLoading} />
               </div>
             </FramedExample>
 
             <FramedExample height={254}
                            summary="Desktop room conversation">
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={roomStore} />
               </div>
             </FramedExample>
 
             <FramedExample dashed={true}
                            height={394}
                            summary="Desktop room conversation local face-mute"
                            width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomStore={desktopLocalFaceMuteRoomStore} />
               </div>
             </FramedExample>
 
             <FramedExample dashed={true} height={394}
                            summary="Desktop room conversation remote face-mute"
                            width={298} >
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   roomStore={desktopRemoteFaceMuteRoomStore} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="StandaloneRoomView">
             <FramedExample cssClass="standalone"
                            dashed={true}
@@ -1169,30 +1150,16 @@
                   dispatcher={dispatcher}
                   isFirefox={false} />
               </div>
             </FramedExample>
 
             <FramedExample cssClass="standalone"
                            dashed={true}
                            height={483}
-                           summary="Standalone room conversation (feedback)"
-                           width={644}>
-              <div className="standalone">
-                <StandaloneRoomView
-                  activeRoomStore={endedRoomStore}
-                  dispatcher={dispatcher}
-                  feedbackStore={feedbackStore}
-                  isFirefox={false} />
-              </div>
-            </FramedExample>
-
-            <FramedExample cssClass="standalone"
-                           dashed={true}
-                           height={483}
                            summary="Standalone room conversation (failed)"
                            width={644} >
               <div className="standalone">
                 <StandaloneRoomView
                   activeRoomStore={failedRoomStore}
                   dispatcher={dispatcher}
                   isFirefox={false} />
               </div>
@@ -1310,17 +1277,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 24;
+      var expectedWarningsCount = 23;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       if (uncaughtError || warningsMismatch) {
         $("#results").append("<div class='failures'><em>" +
           (!!(uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
         if (warningsMismatch) {
           $("#results").append("<li class='test fail'>" +
             "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
             "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -269,38 +269,23 @@ legal_text_tos = Terms of Use
 legal_text_privacy = Privacy Notice
 
 ## LOCALIZATION NOTE (powered_by_beforeLogo, powered_by_afterLogo):
 ## These 2 strings are displayed before and after a 'Telefonica'
 ## logo.
 powered_by_beforeLogo=Powered by
 powered_by_afterLogo=
 
-feedback_call_experience_heading2=How was your conversation?
-feedback_thank_you_heading=Thank you for your feedback!
-feedback_category_list_heading=What made you sad?
-feedback_category_audio_quality=Audio quality
-feedback_category_video_quality=Video quality
-feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing2=Confusing controls
-feedback_category_other2=Other
-feedback_custom_category_text_placeholder=What went wrong?
-feedback_submit_button=Submit
-feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in2):
-## Semicolon-separated list of plural forms. See:
-## http://developer.mozilla.org/en/docs/Localization_and_Plurals
-## In this item, don't translate the part between {{..}}
-feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
 ## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
 ## a signed-in to signed-in user call.
 feedback_rejoin_button=Rejoin
 ## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
 ## an abusive user.
 feedback_report_user_button=Report User
+feedback_window_heading=How was your conversation?
 feedback_request_button=Leave Feedback
 
 help_label=Help
 tour_label=Tour
 
 ## LOCALIZATION NOTE(rooms_default_room_name_template): {{conversationLabel}}
 ## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
 rooms_default_room_name_template=Conversation {{conversationLabel}}