Bug 1106852 - Introducing StoreMixin for Loop. r=Standard8
authorNicolas Perriault <nperriault@mozilla.com>
Thu, 22 Jan 2015 17:39:07 +0100
changeset 225265 ef61f646f36107432d5087395d1de3a08630ac15
parent 225264 609564eb346d35864c46a6d68b3086ca47609266
child 225266 9145bbf29f05df54135a52e932a04bc556be059a
push id28158
push userryanvm@gmail.com
push dateFri, 23 Jan 2015 17:20:14 +0000
treeherdermozilla-central@a1348d5a02b2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1106852
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1106852 - Introducing StoreMixin for Loop. r=Standard8
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/js/feedbackViews.js
browser/components/loop/content/shared/js/feedbackViews.jsx
browser/components/loop/content/shared/js/store.js
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/test/desktop-local/conversationViews_test.js
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/test/shared/feedbackViews_test.js
browser/components/loop/test/standalone/standaloneRoomViews_test.js
browser/components/loop/test/standalone/webapp_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -37,19 +37,17 @@ loop.conversation = (function(mozL10n) {
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for flux style
       conversationAppStore: React.PropTypes.instanceOf(
         loop.store.ConversationAppStore).isRequired,
       conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
                               .isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
       return this.props.conversationAppStore.getStoreState();
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.conversationAppStore, "change", function() {
@@ -63,32 +61,29 @@ loop.conversation = (function(mozL10n) {
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (React.createElement(IncomingConversationView, {
             client: this.props.client, 
             conversation: this.props.conversation, 
             sdk: this.props.sdk, 
-            conversationAppStore: this.props.conversationAppStore, 
-            feedbackStore: this.props.feedbackStore}
+            conversationAppStore: this.props.conversationAppStore}
           ));
         }
         case "outgoing": {
           return (React.createElement(OutgoingConversationView, {
             store: this.props.conversationStore, 
-            dispatcher: this.props.dispatcher, 
-            feedbackStore: this.props.feedbackStore}
+            dispatcher: this.props.dispatcher}
           ));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
-            roomStore: this.props.roomStore, 
-            feedbackStore: this.props.feedbackStore}
+            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.
@@ -150,16 +145,18 @@ loop.conversation = (function(mozL10n) {
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
+    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: window.OT,
       mozLoop: navigator.mozLoop
     });
 
     // Obtain the windowId and pass it through
@@ -181,17 +178,16 @@ loop.conversation = (function(mozL10n) {
       navigator.mozLoop.calls.clearCallInProgress(windowId);
 
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(React.createElement(AppControllerView, {
       conversationAppStore: conversationAppStore, 
       roomStore: roomStore, 
-      feedbackStore: feedbackStore, 
       conversationStore: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -37,19 +37,17 @@ loop.conversation = (function(mozL10n) {
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for flux style
       conversationAppStore: React.PropTypes.instanceOf(
         loop.store.ConversationAppStore).isRequired,
       conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
                               .isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
       return this.props.conversationAppStore.getStoreState();
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.conversationAppStore, "change", function() {
@@ -64,31 +62,28 @@ loop.conversation = (function(mozL10n) {
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (<IncomingConversationView
             client={this.props.client}
             conversation={this.props.conversation}
             sdk={this.props.sdk}
             conversationAppStore={this.props.conversationAppStore}
-            feedbackStore={this.props.feedbackStore}
           />);
         }
         case "outgoing": {
           return (<OutgoingConversationView
             store={this.props.conversationStore}
             dispatcher={this.props.dispatcher}
-            feedbackStore={this.props.feedbackStore}
           />);
         }
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
             roomStore={this.props.roomStore}
-            feedbackStore={this.props.feedbackStore}
           />);
         }
         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.
@@ -150,16 +145,18 @@ loop.conversation = (function(mozL10n) {
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
+    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: window.OT,
       mozLoop: navigator.mozLoop
     });
 
     // Obtain the windowId and pass it through
@@ -181,17 +178,16 @@ loop.conversation = (function(mozL10n) {
       navigator.mozLoop.calls.clearCallInProgress(windowId);
 
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(<AppControllerView
       conversationAppStore={conversationAppStore}
       roomStore={roomStore}
-      feedbackStore={feedbackStore}
       conversationStore={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -343,19 +343,17 @@ loop.conversationViews = (function(mozL1
     mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
 
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
       conversationAppStore: React.PropTypes.instanceOf(
-        loop.store.ConversationAppStore).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+        loop.store.ConversationAppStore).isRequired
     },
 
     getInitialState: function() {
       return {
         callFailed: false, // XXX this should be removed when bug 1047410 lands.
         callStatus: "start"
       };
     },
@@ -421,17 +419,16 @@ loop.conversationViews = (function(mozL1
           }
 
           document.title = mozL10n.get("conversation_has_ended");
 
           this.play("terminated");
 
           return (
             React.createElement(sharedViews.FeedbackView, {
-              feedbackStore: this.props.feedbackStore, 
               onAfterFeedbackReceived: this.closeWindow.bind(this)}
             )
           );
         }
         case "close": {
           this.closeWindow();
           return (React.createElement("div", null));
         }
@@ -924,18 +921,17 @@ loop.conversationViews = (function(mozL1
     mixins: [
       sharedMixins.AudioMixin,
       Backbone.Events
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
+        loop.store.ConversationStore).isRequired
     },
 
     getInitialState: function() {
       return this.props.store.getStoreState();
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.store, "change", function() {
@@ -964,17 +960,16 @@ loop.conversationViews = (function(mozL1
     /**
      * Used to setup and render the feedback view.
      */
     _renderFeedbackView: function() {
       document.title = mozL10n.get("conversation_has_ended");
 
       return (
         React.createElement(sharedViews.FeedbackView, {
-          feedbackStore: this.props.feedbackStore, 
           onAfterFeedbackReceived: this._closeWindow.bind(this)}
         )
       );
     },
 
     render: function() {
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -343,19 +343,17 @@ loop.conversationViews = (function(mozL1
     mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
 
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
       conversationAppStore: React.PropTypes.instanceOf(
-        loop.store.ConversationAppStore).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+        loop.store.ConversationAppStore).isRequired
     },
 
     getInitialState: function() {
       return {
         callFailed: false, // XXX this should be removed when bug 1047410 lands.
         callStatus: "start"
       };
     },
@@ -421,17 +419,16 @@ loop.conversationViews = (function(mozL1
           }
 
           document.title = mozL10n.get("conversation_has_ended");
 
           this.play("terminated");
 
           return (
             <sharedViews.FeedbackView
-              feedbackStore={this.props.feedbackStore}
               onAfterFeedbackReceived={this.closeWindow.bind(this)}
             />
           );
         }
         case "close": {
           this.closeWindow();
           return (<div/>);
         }
@@ -924,18 +921,17 @@ loop.conversationViews = (function(mozL1
     mixins: [
       sharedMixins.AudioMixin,
       Backbone.Events
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
+        loop.store.ConversationStore).isRequired
     },
 
     getInitialState: function() {
       return this.props.store.getStoreState();
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.store, "change", function() {
@@ -964,17 +960,16 @@ loop.conversationViews = (function(mozL1
     /**
      * Used to setup and render the feedback view.
      */
     _renderFeedbackView: function() {
       document.title = mozL10n.get("conversation_has_ended");
 
       return (
         <sharedViews.FeedbackView
-          feedbackStore={this.props.feedbackStore}
           onAfterFeedbackReceived={this._closeWindow.bind(this)}
         />
       );
     },
 
     render: function() {
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -164,19 +164,17 @@ loop.roomViews = (function(mozL10n) {
     mixins: [
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
       sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     _renderInvitationOverlay: function() {
       if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
         return React.createElement(DesktopRoomInvitationView, {
           roomStore: this.props.roomStore, 
           dispatcher: this.props.dispatcher}
         );
@@ -247,17 +245,16 @@ loop.roomViews = (function(mozL10n) {
           //       FULL case should never happen on desktop.
           return React.createElement(loop.conversationViews.GenericFailureView, {
             cancelCall: this.closeWindow}
           );
         }
         case ROOM_STATES.ENDED: {
           if (this.state.used)
             return React.createElement(sharedViews.FeedbackView, {
-              feedbackStore: this.props.feedbackStore, 
               onAfterFeedbackReceived: this.closeWindow}
             );
 
           // In case the room was not used (no one was here), we
           // bypass the feedback form.
           this.closeWindow();
           return null;
         }
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -164,19 +164,17 @@ loop.roomViews = (function(mozL10n) {
     mixins: [
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
       sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     _renderInvitationOverlay: function() {
       if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
         return <DesktopRoomInvitationView
           roomStore={this.props.roomStore}
           dispatcher={this.props.dispatcher}
         />;
@@ -247,17 +245,16 @@ loop.roomViews = (function(mozL10n) {
           //       FULL case should never happen on desktop.
           return <loop.conversationViews.GenericFailureView
             cancelCall={this.closeWindow}
           />;
         }
         case ROOM_STATES.ENDED: {
           if (this.state.used)
             return <sharedViews.FeedbackView
-              feedbackStore={this.props.feedbackStore}
               onAfterFeedbackReceived={this.closeWindow}
             />;
 
           // In case the room was not used (no one was here), we
           // bypass the feedback form.
           this.closeWindow();
           return null;
         }
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ b/browser/components/loop/content/shared/js/feedbackViews.js
@@ -211,60 +211,50 @@ loop.shared.views.FeedbackView = (functi
       );
     }
   });
 
   /**
    * Feedback view.
    */
   var FeedbackView = React.createClass({displayName: "FeedbackView",
-    mixins: [Backbone.Events],
+    mixins: [
+      Backbone.Events,
+      loop.store.StoreMixin("feedbackStore")
+    ],
 
     propTypes: {
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
       onAfterFeedbackReceived: React.PropTypes.func,
       // Used by the UI showcase.
       feedbackState: React.PropTypes.string
     },
 
     getInitialState: function() {
-      var storeState = this.props.feedbackStore.getStoreState();
+      var storeState = this.getStoreState();
       return _.extend({}, storeState, {
         feedbackState: this.props.feedbackState || storeState.feedbackState
       });
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.feedbackStore, "change", this._onStoreStateChanged);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.feedbackStore);
-    },
-
-    _onStoreStateChanged: function() {
-      this.setState(this.props.feedbackStore.getStoreState());
-    },
-
     reset: function() {
-      this.setState(this.props.feedbackStore.getInitialStoreState());
+      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.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
+      this.getStore().dispatchAction(new sharedActions.SendFeedback({
         happy: true,
         category: "",
         description: ""
       }));
     },
 
     handleSadClick: function() {
-      this.props.feedbackStore.dispatchAction(
+      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);
       }
@@ -285,17 +275,17 @@ loop.shared.views.FeedbackView = (functi
                         onClick: this.handleSadClick})
               )
             )
           );
         }
         case FEEDBACK_STATES.DETAILS: {
           return (
             React.createElement(FeedbackForm, {
-              feedbackStore: this.props.feedbackStore, 
+              feedbackStore: this.getStore(), 
               reset: this.reset, 
               pending: this.state.feedbackState === FEEDBACK_STATES.PENDING})
             );
         }
         case FEEDBACK_STATES.PENDING:
         case FEEDBACK_STATES.SENT:
         case FEEDBACK_STATES.FAILED: {
           if (this.state.error) {
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ b/browser/components/loop/content/shared/js/feedbackViews.jsx
@@ -211,60 +211,50 @@ loop.shared.views.FeedbackView = (functi
       );
     }
   });
 
   /**
    * Feedback view.
    */
   var FeedbackView = React.createClass({
-    mixins: [Backbone.Events],
+    mixins: [
+      Backbone.Events,
+      loop.store.StoreMixin("feedbackStore")
+    ],
 
     propTypes: {
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
       onAfterFeedbackReceived: React.PropTypes.func,
       // Used by the UI showcase.
       feedbackState: React.PropTypes.string
     },
 
     getInitialState: function() {
-      var storeState = this.props.feedbackStore.getStoreState();
+      var storeState = this.getStoreState();
       return _.extend({}, storeState, {
         feedbackState: this.props.feedbackState || storeState.feedbackState
       });
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.feedbackStore, "change", this._onStoreStateChanged);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.feedbackStore);
-    },
-
-    _onStoreStateChanged: function() {
-      this.setState(this.props.feedbackStore.getStoreState());
-    },
-
     reset: function() {
-      this.setState(this.props.feedbackStore.getInitialStoreState());
+      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.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
+      this.getStore().dispatchAction(new sharedActions.SendFeedback({
         happy: true,
         category: "",
         description: ""
       }));
     },
 
     handleSadClick: function() {
-      this.props.feedbackStore.dispatchAction(
+      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);
       }
@@ -285,17 +275,17 @@ loop.shared.views.FeedbackView = (functi
                         onClick={this.handleSadClick}></button>
               </div>
             </FeedbackLayout>
           );
         }
         case FEEDBACK_STATES.DETAILS: {
           return (
             <FeedbackForm
-              feedbackStore={this.props.feedbackStore}
+              feedbackStore={this.getStore()}
               reset={this.reset}
               pending={this.state.feedbackState === FEEDBACK_STATES.PENDING} />
             );
         }
         case FEEDBACK_STATES.PENDING:
         case FEEDBACK_STATES.SENT:
         case FEEDBACK_STATES.FAILED: {
           if (this.state.error) {
--- a/browser/components/loop/content/shared/js/store.js
+++ b/browser/components/loop/content/shared/js/store.js
@@ -100,8 +100,48 @@ loop.store.createStore = (function() {
                                    Backbone.Events,
                                    baseStorePrototype,
                                    storeProto);
     return BaseStore;
   }
 
   return createStore;
 })();
+
+/**
+ * Store mixin generator. Usage:
+ *
+ *     StoreMixin.register({roomStore: new RoomStore(…)});
+ *     var Comp = React.createClass({
+ *       mixins: [StoreMixin("roomStore")]
+ *     });
+ */
+loop.store.StoreMixin = (function() {
+  var _stores = {};
+  function StoreMixin(id) {
+    function _getStore() {
+      if (!_stores[id]) {
+        throw new Error("Unavailable store " + id);
+      }
+      return _stores[id];
+    }
+    return {
+      getStore: function() {
+        return _getStore();
+      },
+      getStoreState: function() {
+        return this.getStore().getStoreState();
+      },
+      componentWillMount: function() {
+        this.getStore().on("change", function() {
+          this.setState(this.getStoreState());
+        }, this);
+      },
+      componentWillUnmount: function() {
+        this.getStore().off("change");
+      }
+    };
+  }
+  StoreMixin.register = function(stores) {
+    _.extend(_stores, stores);
+  };
+  return StoreMixin;
+})();
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -18,19 +18,17 @@ loop.standaloneRoomViews = (function(moz
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({displayName: "StandaloneRoomInfoArea",
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
-      ]).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+      ]).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));
     },
@@ -115,17 +113,16 @@ loop.standaloneRoomViews = (function(moz
             )
           );
         }
         case ROOM_STATES.ENDED: {
           if (this.props.roomUsed)
             return (
               React.createElement("div", {className: "ended-conversation"}, 
                 React.createElement(sharedViews.FeedbackView, {
-                  feedbackStore: this.props.feedbackStore, 
                   onAfterFeedbackReceived: this.onFeedbackSent}
                 )
               )
             );
 
           // In case the room was not used (no one was here), we
           // bypass the feedback form.
           this.onFeedbackSent();
@@ -198,18 +195,16 @@ loop.standaloneRoomViews = (function(moz
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
@@ -311,17 +306,16 @@ loop.standaloneRoomViews = (function(moz
         React.createElement("div", {className: "room-conversation-wrapper"}, 
           React.createElement("div", {className: "beta-logo"}), 
           React.createElement(StandaloneRoomHeader, null), 
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
                                   helper: this.props.helper, 
                                   activeRoomStore: this.props.activeRoomStore, 
-                                  feedbackStore: this.props.feedbackStore, 
                                   roomUsed: this.state.used}), 
           React.createElement("div", {className: "video-layout-wrapper"}, 
             React.createElement("div", {className: "conversation room-conversation"}, 
               React.createElement("h2", {className: "room-name"}, this.state.roomName), 
               React.createElement("div", {className: "media nested"}, 
                 React.createElement("span", {className: "self-view-hidden-message"}, 
                   mozL10n.get("self_view_hidden_message")
                 ), 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -18,19 +18,17 @@ loop.standaloneRoomViews = (function(moz
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
-      ]).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+      ]).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));
     },
@@ -115,17 +113,16 @@ loop.standaloneRoomViews = (function(moz
             </div>
           );
         }
         case ROOM_STATES.ENDED: {
           if (this.props.roomUsed)
             return (
               <div className="ended-conversation">
                 <sharedViews.FeedbackView
-                  feedbackStore={this.props.feedbackStore}
                   onAfterFeedbackReceived={this.onFeedbackSent}
                 />
               </div>
             );
 
           // In case the room was not used (no one was here), we
           // bypass the feedback form.
           this.onFeedbackSent();
@@ -198,18 +195,16 @@ loop.standaloneRoomViews = (function(moz
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
@@ -311,17 +306,16 @@ loop.standaloneRoomViews = (function(moz
         <div className="room-conversation-wrapper">
           <div className="beta-logo" />
           <StandaloneRoomHeader />
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
                                   helper={this.props.helper}
                                   activeRoomStore={this.props.activeRoomStore}
-                                  feedbackStore={this.props.feedbackStore}
                                   roomUsed={this.state.used} />
           <div className="video-layout-wrapper">
             <div className="conversation room-conversation">
               <h2 className="room-name">{this.state.roomName}</h2>
               <div className="media nested">
                 <span className="self-view-hidden-message">
                   {mozL10n.get("self_view_hidden_message")}
                 </span>
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -558,28 +558,26 @@ loop.webapp = (function($, _, OT, mozL10
   /**
    * Ended conversation view.
    */
   var EndedConversationView = React.createClass({displayName: "EndedConversationView",
     propTypes: {
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
       onAfterFeedbackReceived: React.PropTypes.func.isRequired
     },
 
     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, {
-            feedbackStore: this.props.feedbackStore, 
             onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
           ), 
           React.createElement(sharedViews.ConversationView, {
             initiate: false, 
             sdk: this.props.sdk, 
             model: this.props.conversation, 
             audio: {enabled: false, visible: false}, 
             video: {enabled: false, visible: false}}
@@ -626,21 +624,21 @@ loop.webapp = (function($, _, OT, mozL10
    */
   var OutgoingConversationView = React.createClass({displayName: "OutgoingConversationView",
     propTypes: {
       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
       conversation: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(sharedModels.ConversationModel),
         React.PropTypes.instanceOf(FxOSConversationModel)
       ]).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
-      sdk: React.PropTypes.object.isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
+      sdk: React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         callStatus: "start"
       };
     },
 
@@ -661,17 +659,17 @@ 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.feedbackStore.dispatchAction(new sharedActions.FeedbackComplete());
+      this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
       return function() {
         this.setState({callStatus: "start"});
       }.bind(this);
     },
 
     /**
      * Renders the conversation views.
      */
@@ -714,17 +712,16 @@ loop.webapp = (function($, _, OT, mozL10
             )
           );
         }
         case "end": {
           return (
             React.createElement(EndedConversationView, {
               sdk: this.props.sdk, 
               conversation: this.props.conversation, 
-              feedbackStore: this.props.feedbackStore, 
               onAfterFeedbackReceived: this.resetCallStatus()}
             )
           );
         }
         case "expired": {
           return (
             React.createElement(CallUrlExpiredView, {helper: this.props.helper})
           );
@@ -937,18 +934,17 @@ loop.webapp = (function($, _, OT, mozL10
 
       // XXX New types for flux style
       standaloneAppStore: React.PropTypes.instanceOf(
         loop.store.StandaloneAppStore).isRequired,
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return this.props.standaloneAppStore.getStoreState();
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.standaloneAppStore, "change", function() {
@@ -971,29 +967,28 @@ loop.webapp = (function($, _, OT, mozL10
         }
         case "unsupportedBrowser": {
           return React.createElement(UnsupportedBrowserView, {helper: this.props.helper});
         }
         case "outgoing": {
           return (
             React.createElement(OutgoingConversationView, {
                client: this.props.client, 
+               dispatcher: this.props.dispatcher, 
                conversation: this.props.conversation, 
                helper: this.props.helper, 
                notifications: this.props.notifications, 
-               sdk: this.props.sdk, 
-               feedbackStore: this.props.feedbackStore}
+               sdk: this.props.sdk}
             )
           );
         }
         case "room": {
           return (
             React.createElement(loop.standaloneRoomViews.StandaloneRoomView, {
               activeRoomStore: this.props.activeRoomStore, 
-              feedbackStore: this.props.feedbackStore, 
               dispatcher: this.props.dispatcher, 
               helper: this.props.helper}
             )
           );
         }
         case "home": {
           return React.createElement(HomeView, null);
         }
@@ -1070,27 +1065,28 @@ loop.webapp = (function($, _, OT, mozL10
       dispatcher: dispatcher,
       helper: helper,
       sdk: OT
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
+    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(React.createElement(WebappRootView, {
       client: client, 
       conversation: conversation, 
       helper: helper, 
       notifications: notifications, 
       sdk: OT, 
-      feedbackStore: feedbackStore, 
       standaloneAppStore: standaloneAppStore, 
       activeRoomStore: activeRoomStore, 
       dispatcher: dispatcher}
     ), document.querySelector("#main"));
 
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = mozL10n.language.code;
     document.documentElement.dir = mozL10n.language.direction;
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -558,28 +558,26 @@ loop.webapp = (function($, _, OT, mozL10
   /**
    * Ended conversation view.
    */
   var EndedConversationView = React.createClass({
     propTypes: {
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
       onAfterFeedbackReceived: React.PropTypes.func.isRequired
     },
 
     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
-            feedbackStore={this.props.feedbackStore}
             onAfterFeedbackReceived={this.props.onAfterFeedbackReceived}
           />
           <sharedViews.ConversationView
             initiate={false}
             sdk={this.props.sdk}
             model={this.props.conversation}
             audio={{enabled: false, visible: false}}
             video={{enabled: false, visible: false}}
@@ -626,21 +624,21 @@ loop.webapp = (function($, _, OT, mozL10
    */
   var OutgoingConversationView = React.createClass({
     propTypes: {
       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
       conversation: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(sharedModels.ConversationModel),
         React.PropTypes.instanceOf(FxOSConversationModel)
       ]).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
-      sdk: React.PropTypes.object.isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
+      sdk: React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         callStatus: "start"
       };
     },
 
@@ -661,17 +659,17 @@ 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.feedbackStore.dispatchAction(new sharedActions.FeedbackComplete());
+      this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
       return function() {
         this.setState({callStatus: "start"});
       }.bind(this);
     },
 
     /**
      * Renders the conversation views.
      */
@@ -714,17 +712,16 @@ loop.webapp = (function($, _, OT, mozL10
             />
           );
         }
         case "end": {
           return (
             <EndedConversationView
               sdk={this.props.sdk}
               conversation={this.props.conversation}
-              feedbackStore={this.props.feedbackStore}
               onAfterFeedbackReceived={this.resetCallStatus()}
             />
           );
         }
         case "expired": {
           return (
             <CallUrlExpiredView helper={this.props.helper} />
           );
@@ -937,18 +934,17 @@ loop.webapp = (function($, _, OT, mozL10
 
       // XXX New types for flux style
       standaloneAppStore: React.PropTypes.instanceOf(
         loop.store.StandaloneAppStore).isRequired,
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return this.props.standaloneAppStore.getStoreState();
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.standaloneAppStore, "change", function() {
@@ -971,29 +967,28 @@ loop.webapp = (function($, _, OT, mozL10
         }
         case "unsupportedBrowser": {
           return <UnsupportedBrowserView helper={this.props.helper}/>;
         }
         case "outgoing": {
           return (
             <OutgoingConversationView
                client={this.props.client}
+               dispatcher={this.props.dispatcher}
                conversation={this.props.conversation}
                helper={this.props.helper}
                notifications={this.props.notifications}
                sdk={this.props.sdk}
-               feedbackStore={this.props.feedbackStore}
             />
           );
         }
         case "room": {
           return (
             <loop.standaloneRoomViews.StandaloneRoomView
               activeRoomStore={this.props.activeRoomStore}
-              feedbackStore={this.props.feedbackStore}
               dispatcher={this.props.dispatcher}
               helper={this.props.helper}
             />
           );
         }
         case "home": {
           return <HomeView />;
         }
@@ -1070,27 +1065,28 @@ loop.webapp = (function($, _, OT, mozL10
       dispatcher: dispatcher,
       helper: helper,
       sdk: OT
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
+    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(<WebappRootView
       client={client}
       conversation={conversation}
       helper={helper}
       notifications={notifications}
       sdk={OT}
-      feedbackStore={feedbackStore}
       standaloneAppStore={standaloneAppStore}
       activeRoomStore={activeRoomStore}
       dispatcher={dispatcher}
     />, document.querySelector("#main"));
 
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = mozL10n.language.code;
     document.documentElement.dir = mozL10n.language.direction;
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -577,18 +577,17 @@ describe("loop.conversationViews", funct
 
   describe("OutgoingConversationView", function() {
     var store, feedbackStore;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OutgoingConversationView, {
           dispatcher: dispatcher,
-          store: store,
-          feedbackStore: feedbackStore
+          store: store
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: fakeMozLoop,
         sdkDriver: {}
@@ -684,18 +683,17 @@ describe("loop.conversationViews", funct
         feedbackStore;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.IncomingConversationView, {
           client: client,
           conversation: conversation,
           sdk: {},
-          conversationAppStore: conversationAppStore,
-          feedbackStore: feedbackStore
+          conversationAppStore: conversationAppStore
         }));
     }
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {}
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -197,27 +197,29 @@ describe("loop.roomViews", function () {
       });
     });
   });
 
   describe("DesktopRoomConversationView", function() {
     var view;
 
     beforeEach(function() {
+      loop.store.StoreMixin.register({
+        feedbackStore: new loop.store.FeedbackStore(dispatcher, {
+          feedbackClient: {}
+        })
+      });
       sandbox.stub(dispatcher, "dispatch");
     });
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.roomViews.DesktopRoomConversationView, {
           dispatcher: dispatcher,
-          roomStore: roomStore,
-          feedbackStore: new loop.store.FeedbackStore(dispatcher, {
-            feedbackClient: {}
-          })
+          roomStore: roomStore
         }));
     }
 
     it("should dispatch a setMute action when the audio mute button is pressed",
       function() {
         view = mountTestComponent();
 
         view.setState({audioMuted: true});
--- a/browser/components/loop/test/shared/feedbackViews_test.js
+++ b/browser/components/loop/test/shared/feedbackViews_test.js
@@ -20,20 +20,19 @@ describe("loop.shared.views.FeedbackView
 
   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, {
-        feedbackStore: feedbackStore
-      }));
+      React.createElement(sharedViews.FeedbackView));
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   // local test helpers
   function clickHappyFace(comp) {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -21,16 +21,17 @@ describe("loop.standaloneRoomViews", fun
     dispatch = sandbox.stub(dispatcher, "dispatch");
     activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: {},
       sdkDriver: {}
     });
     feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: {}
     });
+    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
 
     sandbox.useFakeTimers();
 
     // Prevents audio request errors in the test console.
     sandbox.useFakeXMLHttpRequest();
   });
 
   afterEach(function() {
@@ -39,17 +40,16 @@ describe("loop.standaloneRoomViews", fun
 
   describe("StandaloneRoomView", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomView, {
             dispatcher: dispatcher,
             activeRoomStore: activeRoomStore,
-            feedbackStore: feedbackStore,
             helper: new loop.shared.utils.Helper()
           }));
     }
 
     function expectActionDispatched(view) {
       sinon.assert.calledOnce(dispatch);
       sinon.assert.calledWithExactly(dispatch,
         sinon.match.instanceOf(sharedActions.SetupStreamElements));
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -15,25 +15,26 @@ describe("loop.webapp", function() {
       sharedViews = loop.shared.views,
       sharedUtils = loop.shared.utils,
       standaloneMedia = loop.standaloneMedia,
       sandbox,
       notifications,
       stubGetPermsAndCacheMedia,
       fakeAudioXHR,
       dispatcher,
-      feedbackStore,
       WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     notifications = new sharedModels.NotificationCollection();
-    feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: {}
+    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() {},
@@ -122,17 +123,17 @@ describe("loop.webapp", function() {
       });
       conversation.set("loopToken", "fakeToken");
       ocView = mountTestComponent({
         helper: new sharedUtils.Helper(),
         client: client,
         conversation: conversation,
         notifications: notifications,
         sdk: {},
-        feedbackStore: feedbackStore
+        dispatcher: dispatcher
       });
     });
 
     describe("start", function() {
       it("should display the StartConversationView", function() {
         TestUtils.findRenderedComponentWithType(ocView,
           loop.webapp.StartConversationView);
       });
@@ -647,22 +648,22 @@ describe("loop.webapp", function() {
     var activeRoomStore;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.webapp.WebappRootView, {
             client: client,
             helper: helper,
+            dispatcher: dispatcher,
             notifications: notifications,
             sdk: sdk,
             conversation: conversationModel,
             standaloneAppStore: standaloneAppStore,
-            activeRoomStore: activeRoomStore,
-            feedbackStore: feedbackStore
+            activeRoomStore: activeRoomStore
           }));
     }
 
     beforeEach(function() {
       helper = new sharedUtils.Helper();
       sdk = {
         checkSystemRequirements: function() { return true; }
       };
@@ -1080,17 +1081,16 @@ describe("loop.webapp", function() {
         sdk: {}
       });
       sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
       view = React.addons.TestUtils.renderIntoDocument(
         React.createElement(
           loop.webapp.EndedConversationView, {
             conversation: conversation,
             sdk: {},
-            feedbackStore: feedbackStore,
             onAfterFeedbackReceived: function(){}
           }));
     });
 
     it("should render a ConversationView", function() {
       TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
     });
 
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -75,16 +75,18 @@
     feedbackClient: stageFeedbackApiClient
   });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
     sdkDriver: {}
   });
 
+  loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -75,16 +75,18 @@
     feedbackClient: stageFeedbackApiClient
   });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
     sdkDriver: {}
   });
 
+  loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"