Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 18 Dec 2014 17:27:16 -0500
changeset 246212 9acb15a52030764ee4dca78905995f6b9edecd15
parent 246190 121e801ae0441d04499441b103829c5029cdb363 (current diff)
parent 246211 803bc910c45a875d9d76dc689c45dd91a1e02e23 (diff)
child 246245 1427b365cd39118bcec66d232c62d4c806b66b46
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.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
Merge fx-team to m-c. a=merge
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -348,17 +348,17 @@ input[type=button] {
 #newtab-search-logo.magnifier {
   width: 38px; /* 26 image width + 6 left "padding" + 6 right "padding" */
   -moz-margin-end: 5px;
   background-size: 26px;
   background-image: url("chrome://browser/skin/magnifier.png");
 }
 
 @media not all and (max-resolution: 1dppx) {
-  #newtab-search-icon.magnifier {
+  #newtab-search-logo.magnifier {
     background-image: url("chrome://browser/skin/magnifier@2x.png");
   }
 }
 
 #newtab-search-logo[type="logo"] {
   background-size: 65px 26px;
   width: 77px; /* 65 image width + 6 left "padding" + 6 right "padding" */
 }
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1035,16 +1035,18 @@
         }
 
         const kBundleURI = "chrome://browser/locale/search.properties";
         let bundle = Services.strings.createBundle(kBundleURI);
         let headerText = bundle.formatStringFromName("searchHeader",
                                                      [currentEngine.name], 1);
         document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
                 .setAttribute("value", headerText);
+        document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
+                .engine = currentEngine;
 
         // Update the 'Search for <keywords> with:" header.
         let headerSearchText =
           document.getAnonymousElementByAttribute(this, "anonid",
                                                   "searchbar-oneoffheader-searchtext");
         let textbox = searchbar.textbox;
         let self = this;
         let inputHandler = function() {
@@ -1193,21 +1195,23 @@
         }
       ]]></handler>
 
       <handler event="click"><![CDATA[
         if (event.button == 2)
           return; // ignore right clicks.
 
         let button = event.originalTarget;
-        if (button.localName != "button" || !button.engine)
+        let engine = button.engine || button.parentNode.engine;
+
+        if (!engine)
           return;
 
         let searchbar = document.getElementById("searchbar");
-        searchbar.handleSearchCommand(event, button.engine);
+        searchbar.handleSearchCommand(event, engine);
       ]]></handler>
 
       <handler event="command"><![CDATA[
         let target = event.originalTarget;
         if (target.classList.contains("addengine-item")) {
           // On success, hide and reshow the panel to show the new engine.
           let installCallback = {
             onSuccess: function(engine) {
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -110,16 +110,20 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::ENABLE_INDEXED_DB },
   { "looppanel", "chrome://browser/content/loop/panel.html",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT |
     nsIAboutModule::ENABLE_INDEXED_DB,
     // Shares an IndexedDB origin with about:loopconversation.
     "loopconversation" },
+  { "reader", "chrome://global/content/reader/aboutReader.html",
+    nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+    nsIAboutModule::ALLOW_SCRIPT |
+    nsIAboutModule::HIDE_FROM_ABOUTABOUT },
 };
 static const int kRedirTotal = ArrayLength(kRedirMap);
 
 static nsAutoCString
 GetAboutModuleName(nsIURI *aURI)
 {
   nsAutoCString path;
   aURI->GetPath(path);
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -110,16 +110,17 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #ifdef MOZ_SERVICES_HEALTHREPORT
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "app-manager", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "customizing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "reader", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN)
     { NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
 #elif defined(XP_MACOSX)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
     { nullptr }
 };
 
--- a/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js
+++ b/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js
@@ -37,17 +37,21 @@ add_task(function() {
   FullZoom.reset();
   yield zoomResetPromise;
   is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:mozilla");
 
   // Test zoom label updates while navigating pages in the same tab.
   FullZoom.enlarge();
   yield zoomChangePromise;
   is(parseInt(zoomResetButton.label, 10), 110, "Zoom is changed to 110% for about:mozilla");
+  let attributeChangePromise = promiseAttributeMutation(zoomResetButton, "label", (v) => {
+    return parseInt(v, 10) == 100;
+  });
   yield promiseTabLoadEvent(tab1, "about:home");
+  yield attributeChangePromise;
   is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:home");
   yield promiseTabHistoryNavigation(-1, function() {
     return parseInt(zoomResetButton.label, 10) == 110;
   });
   is(parseInt(zoomResetButton.label, 10), 110, "Zoom is still 110% for about:mozilla");
 });
 
 function promiseObserverNotification(aObserver) {
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -456,16 +456,44 @@ function promiseTabHistoryNavigation(aDi
   }
   gBrowser.addEventListener("pageshow", listener, true);
 
   content.history.go(aDirection);
 
   return deferred.promise;
 }
 
+/**
+ * Wait for an attribute on a node to change
+ *
+ * @param aNode      Node on which the mutation is expected
+ * @param aAttribute The attribute we're interested in
+ * @param aFilterFn  A function to check if the new value is what we want.
+ * @return {Promise} resolved when the requisite mutation shows up.
+ */
+function promiseAttributeMutation(aNode, aAttribute, aFilterFn) {
+  return new Promise((resolve, reject) => {
+    info("waiting for mutation of attribute '" + aAttribute + "'.");
+    let obs = new MutationObserver((mutations) => {
+      for (let mut of mutations) {
+        let attr = mut.attributeName;
+        let newValue = mut.target.getAttribute(attr);
+        if (aFilterFn(newValue)) {
+          ok(true, "mutation occurred: attribute '" + attr + "' changed to '" + newValue + "' from '" + mut.oldValue + "'.");
+          obs.disconnect();
+          resolve();
+        } else {
+          info("Ignoring mutation that produced value " + newValue + " because of filter.");
+        }
+      }
+    });
+    obs.observe(aNode, {attributeFilter: [aAttribute]});
+  });
+}
+
 function popupShown(aPopup) {
   return promisePopupEvent(aPopup, "shown");
 }
 
 function popupHidden(aPopup) {
   return promisePopupEvent(aPopup, "hidden");
 }
 
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -32,16 +32,18 @@
     <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/roomStore.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/feedbackViews.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>
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -725,16 +725,25 @@ loop.panel = (function(_, mozL10n) {
       // 1074665.
       this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.store);
     },
 
+    componentWillUpdate: function(nextProps, nextState) {
+      // If we've just created a room, close the panel - the store will open
+      // the room.
+      if (this.state.pendingCreation &&
+          !nextState.pendingCreation && !nextState.error) {
+        this.closeWindow();
+      }
+    },
+
     _onStoreStateChanged: function() {
       this.setState(this.props.store.getStoreState());
     },
 
     _getListHeading: function() {
       var numRooms = this.state.rooms.length;
       if (numRooms === 0) {
         return mozL10n.get("rooms_list_no_current_conversations");
@@ -742,18 +751,16 @@ loop.panel = (function(_, mozL10n) {
       return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
     },
 
     _hasPendingOperation: function() {
       return this.state.pendingCreation || this.state.pendingInitialRetrieval;
     },
 
     handleCreateButtonClick: function() {
-      this.closeWindow();
-
       this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
         nameTemplate: mozL10n.get("rooms_default_room_name_template"),
         roomOwner: this.props.userDisplayName
       }));
     },
 
     render: function() {
       if (this.state.error) {
@@ -998,17 +1005,18 @@ loop.panel = (function(_, mozL10n) {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     var client = new loop.Client();
     var notifications = new sharedModels.NotificationCollection();
     var dispatcher = new loop.Dispatcher();
     var roomStore = new loop.store.RoomStore(dispatcher, {
-      mozLoop: navigator.mozLoop
+      mozLoop: navigator.mozLoop,
+      notifications: notifications
     });
 
     React.renderComponent(PanelView({
       client: client, 
       notifications: notifications, 
       roomStore: roomStore, 
       mozLoop: navigator.mozLoop, 
       dispatcher: dispatcher}
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -725,16 +725,25 @@ loop.panel = (function(_, mozL10n) {
       // 1074665.
       this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.store);
     },
 
+    componentWillUpdate: function(nextProps, nextState) {
+      // If we've just created a room, close the panel - the store will open
+      // the room.
+      if (this.state.pendingCreation &&
+          !nextState.pendingCreation && !nextState.error) {
+        this.closeWindow();
+      }
+    },
+
     _onStoreStateChanged: function() {
       this.setState(this.props.store.getStoreState());
     },
 
     _getListHeading: function() {
       var numRooms = this.state.rooms.length;
       if (numRooms === 0) {
         return mozL10n.get("rooms_list_no_current_conversations");
@@ -742,18 +751,16 @@ loop.panel = (function(_, mozL10n) {
       return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
     },
 
     _hasPendingOperation: function() {
       return this.state.pendingCreation || this.state.pendingInitialRetrieval;
     },
 
     handleCreateButtonClick: function() {
-      this.closeWindow();
-
       this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
         nameTemplate: mozL10n.get("rooms_default_room_name_template"),
         roomOwner: this.props.userDisplayName
       }));
     },
 
     render: function() {
       if (this.state.error) {
@@ -998,17 +1005,18 @@ loop.panel = (function(_, mozL10n) {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     var client = new loop.Client();
     var notifications = new sharedModels.NotificationCollection();
     var dispatcher = new loop.Dispatcher();
     var roomStore = new loop.store.RoomStore(dispatcher, {
-      mozLoop: navigator.mozLoop
+      mozLoop: navigator.mozLoop,
+      notifications: notifications
     });
 
     React.renderComponent(<PanelView
       client={client}
       notifications={notifications}
       roomStore={roomStore}
       mozLoop={navigator.mozLoop}
       dispatcher={dispatcher}
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -24,14 +24,16 @@
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="loop/shared/js/store.js"></script>
     <script type="text/javascript" src="loop/shared/js/roomStore.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/js/client.js"></script>
     <script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>
     <script type="text/javascript" src="loop/js/panel.js"></script>
  </body>
 </html>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -193,37 +193,51 @@ loop.shared.actions = (function() {
     CreateRoom: Action.define("createRoom", {
       // The localized template to use to name the new room
       // (eg. "Conversation {{conversationLabel}}").
       nameTemplate: String,
       roomOwner: String
     }),
 
     /**
+     * When a room has been created.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    CreatedRoom: Action.define("createdRoom", {
+      roomToken: String
+    }),
+
+    /**
      * Rooms creation error.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     CreateRoomError: Action.define("createRoomError", {
-      error: Error
+      // There's two types of error possible - one thrown by our code (and Error)
+      // and the other is an Object about the error codes from the server as
+      // returned by the Hawk request.
+      error: Object
     }),
 
     /**
      * Deletes a room.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     DeleteRoom: Action.define("deleteRoom", {
       roomToken: String
     }),
 
     /**
      * Room deletion error.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     DeleteRoomError: Action.define("deleteRoomError", {
-      error: Error
+      // There's two types of error possible - one thrown by our code (and Error)
+      // and the other is an Object about the error codes from the server as
+      // returned by the Hawk request.
+      error: Object
     }),
 
     /**
      * Retrieves room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     GetAllRooms: Action.define("getAllRooms", {
     }),
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -16,41 +16,17 @@ loop.store.ActiveRoomStore = (function()
   // Error numbers taken from
   // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
   var SERVER_CODES = loop.store.SERVER_CODES = {
     INVALID_TOKEN: 105,
     EXPIRED: 111,
     ROOM_FULL: 202
   };
 
-  var ROOM_STATES = loop.store.ROOM_STATES = {
-    // The initial state of the room
-    INIT: "room-init",
-    // The store is gathering the room data
-    GATHER: "room-gather",
-    // The store has got the room data
-    READY: "room-ready",
-    // Obtaining media from the user
-    MEDIA_WAIT: "room-media-wait",
-    // The room is known to be joined on the loop-server
-    JOINED: "room-joined",
-    // The room is connected to the sdk server.
-    SESSION_CONNECTED: "room-session-connected",
-    // There are participants in the room.
-    HAS_PARTICIPANTS: "room-has-participants",
-    // There was an issue with the room
-    FAILED: "room-failed",
-    // The room is full
-    FULL: "room-full",
-    // The room conversation has ended
-    ENDED: "room-ended",
-    // The window is closing
-    CLOSING: "room-closing"
-  };
-
+  var ROOM_STATES = loop.store.ROOM_STATES;
   /**
    * Active room 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.
    * - {OTSdkDriver} sdkDriver  The SDK driver instance.
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/fxOSActiveRoomStore.js
@@ -0,0 +1,164 @@
+/* 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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.store = loop.store || {};
+
+loop.store.FxOSActiveRoomStore = (function() {
+  "use strict";
+  var sharedActions = loop.shared.actions;
+  var ROOM_STATES = loop.store.ROOM_STATES;
+
+  var FxOSActiveRoomStore = loop.store.createStore({
+    actions: [
+      "fetchServerData"
+    ],
+
+    initialize: function(options) {
+      if (!options.mozLoop) {
+        throw new Error("Missing option mozLoop");
+      }
+      this._mozLoop = options.mozLoop;
+    },
+
+    /**
+     * Returns initial state data for this active room.
+     */
+    getInitialStoreState: function() {
+      return {
+        roomState: ROOM_STATES.INIT,
+        audioMuted: false,
+        videoMuted: false,
+        failureReason: undefined
+      };
+    },
+
+    /**
+     * Registers the actions with the dispatcher that this store is interested
+     * in after the initial setup has been performed.
+     */
+    _registerPostSetupActions: function() {
+      this.dispatcher.register(this, [
+        "joinRoom"
+      ]);
+    },
+
+    /**
+     * Execute fetchServerData event action from the dispatcher. Although
+     * this is to fetch the server data - for rooms on the standalone client,
+     * we don't actually need to get any data. Therefore we just save the
+     * data that is given to us for when the user chooses to join the room.
+     *
+     * @param {sharedActions.FetchServerData} actionData
+     */
+    fetchServerData: function(actionData) {
+      if (actionData.windowType !== "room") {
+        // Nothing for us to do here, leave it to other stores.
+        return;
+      }
+
+      this._registerPostSetupActions();
+
+      this.setStoreState({
+        roomToken: actionData.token,
+        roomState: ROOM_STATES.READY
+      });
+
+    },
+
+    /**
+     * Handles the action to join to a room.
+     */
+    joinRoom: function() {
+      // Reset the failure reason if necessary.
+      if (this.getStoreState().failureReason) {
+        this.setStoreState({failureReason: undefined});
+      }
+
+      this._setupOutgoingRoom(true);
+    },
+
+    /**
+     * Sets up an outgoing room. It will try launching the activity to let the
+     * FirefoxOS loop app handle the call. If the activity fails:
+     *   - if installApp is true, then it'll try to install the FirefoxOS loop
+     *     app.
+     *   - if installApp is false, then it'll just log and error and fail.
+     *
+     * @param {boolean} installApp
+     */
+    _setupOutgoingRoom: function(installApp) {
+      var request = new MozActivity({
+        name: "room-call",
+        data: {
+          type: "loop/rToken",
+          token: this.getStoreState("roomToken")
+        }
+      });
+
+      request.onsuccess = function() {};
+
+      request.onerror = (function(event) {
+        if (!installApp) {
+          // This really should not happen ever.
+          console.error(
+           "Unexpected activity launch error after the app has been installed");
+          return;
+        }
+        if (event.target.error.name !== "NO_PROVIDER") {
+          console.error ("Unexpected " + event.target.error.name);
+          return;
+        }
+        // We need to install the FxOS app.
+        this.setStoreState({
+            marketplaceSrc: loop.config.marketplaceUrl,
+            onMarketplaceMessage: this._onMarketplaceMessage.bind(this)
+        });
+      }).bind(this);
+    },
+
+    /**
+     * This method will handle events generated on the marketplace frame. It
+     * will launch the FirefoxOS loop app installation, and receive the result
+     * of the installation.
+     *
+     * @param {DOMEvent} event
+     */
+    _onMarketplaceMessage: function(event) {
+      var message = event.data;
+      switch (message.name) {
+        case "loaded":
+          var marketplace = window.document.getElementById("marketplace");
+          // Once we have it loaded, we request the installation of the FxOS
+          // Loop client app. We will be receiving the result of this action
+          // via postMessage from the child iframe.
+          marketplace.contentWindow.postMessage({
+            "name": "install-package",
+            "data": {
+              "product": {
+                "name": loop.config.fxosApp.name,
+                "manifest_url": loop.config.fxosApp.manifestUrl,
+                "is_packaged": true
+              }
+            }
+          }, "*");
+          break;
+        case "install-package":
+          window.removeEventListener("message", this.onMarketplaceMessage);
+          if (message.error) {
+            console.error(message.error.error);
+            return;
+          }
+          // We installed the FxOS app, so we can continue with the call
+          // process.
+          this._setupOutgoingRoom(false);
+          break;
+      }
+    }
+  });
+
+  return FxOSActiveRoomStore;
+})();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/roomStates.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.store = loop.store || {};
+
+loop.store.ROOM_STATES = {
+    // The initial state of the room
+    INIT: "room-init",
+    // The store is gathering the room data
+    GATHER: "room-gather",
+    // The store has got the room data
+    READY: "room-ready",
+    // Obtaining media from the user
+    MEDIA_WAIT: "room-media-wait",
+    // The room is known to be joined on the loop-server
+    JOINED: "room-joined",
+    // The room is connected to the sdk server.
+    SESSION_CONNECTED: "room-session-connected",
+    // There are participants in the room.
+    HAS_PARTICIPANTS: "room-has-participants",
+    // There was an issue with the room
+    FAILED: "room-failed",
+    // The room is full
+    FULL: "room-full",
+    // The room conversation has ended
+    ENDED: "room-ended",
+    // The window is closing
+    CLOSING: "room-closing"
+};
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -2,17 +2,17 @@
  * 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/. */
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.store = loop.store || {};
 
-(function() {
+(function(mozL10n) {
   "use strict";
 
   /**
    * Shared actions.
    * @type {Object}
    */
   var sharedActions = loop.shared.actions;
 
@@ -62,16 +62,18 @@ loop.store = loop.store || {};
    * Room 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.
    * - {ActiveRoomStore} activeRoomStore  An optional substore for active room
    *                                      state.
+   * - {Notifications}   notifications    An optional notifications item that is
+   *                                      required if create actions are to be used
    */
   loop.store.RoomStore = loop.store.createStore({
     /**
      * Maximum size given to createRoom; only 2 is supported (and is
      * always passed) because that's what the user-experience is currently
      * designed and tested to handle.
      * @type {Number}
      */
@@ -84,16 +86,17 @@ loop.store = loop.store || {};
     defaultExpiresIn: DEFAULT_EXPIRES_IN,
 
     /**
      * Registered actions.
      * @type {Array}
      */
     actions: [
       "createRoom",
+      "createdRoom",
       "createRoomError",
       "copyRoomUrl",
       "deleteRoom",
       "deleteRoomError",
       "emailRoomUrl",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
@@ -102,16 +105,17 @@ loop.store = loop.store || {};
       "updateRoomList"
     ],
 
     initialize: function(options) {
       if (!options.mozLoop) {
         throw new Error("Missing option mozLoop");
       }
       this._mozLoop = options.mozLoop;
+      this._notifications = options.notifications;
 
       if (options.activeRoomStore) {
         this.activeRoomStore = options.activeRoomStore;
         this.activeRoomStore.on("change",
                                 this._onActiveRoomStoreChange.bind(this));
       }
     },
 
@@ -252,48 +256,71 @@ loop.store = loop.store || {};
     },
 
     /**
      * Creates a new room.
      *
      * @param {sharedActions.CreateRoom} actionData The new room information.
      */
     createRoom: function(actionData) {
-      this.setStoreState({pendingCreation: true});
+      this.setStoreState({
+        pendingCreation: true,
+        error: null,
+      });
 
       var roomCreationData = {
         roomName:  this._generateNewRoomName(actionData.nameTemplate),
         roomOwner: actionData.roomOwner,
         maxSize:   this.maxRoomCreationSize,
         expiresIn: this.defaultExpiresIn
       };
 
+      this._notifications.remove("create-room-error");
+
       this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) {
-        this.setStoreState({pendingCreation: false});
         if (err) {
           this.dispatchAction(new sharedActions.CreateRoomError({error: err}));
           return;
         }
-        // Opens the newly created room
-        this.dispatchAction(new sharedActions.OpenRoom({
+
+        this.dispatchAction(new sharedActions.CreatedRoom({
           roomToken: createdRoom.roomToken
         }));
       }.bind(this));
     },
 
     /**
+     * Executed when a room has been created
+     */
+    createdRoom: function(actionData) {
+      this.setStoreState({pendingCreation: false});
+
+      // Opens the newly created room
+      this.dispatchAction(new sharedActions.OpenRoom({
+        roomToken: actionData.roomToken
+      }));
+    },
+
+    /**
      * Executed when a room creation error occurs.
      *
      * @param {sharedActions.CreateRoomError} actionData The action data.
      */
     createRoomError: function(actionData) {
       this.setStoreState({
         error: actionData.error,
         pendingCreation: false
       });
+
+      // XXX Needs a more descriptive error - bug 1109151.
+      this._notifications.set({
+        id: "create-room-error",
+        level: "error",
+        message: mozL10n.get("generic_failure_title")
+      });
     },
 
     /**
      * Copy a room url.
      *
      * @param  {sharedActions.CopyRoomUrl} actionData The action data.
      */
     copyRoomUrl: function(actionData) {
@@ -401,9 +428,9 @@ loop.store = loop.store || {};
           }
         }.bind(this));
     },
 
     renameRoomError: function(actionData) {
       this.setStoreState({error: actionData.error});
     }
   });
-})();
+})(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -61,32 +61,34 @@ browser.jar:
   content/browser/loop/shared/img/vivo.png                      (content/shared/img/vivo.png)
   content/browser/loop/shared/img/vivo@2x.png                   (content/shared/img/vivo@2x.png)
   content/browser/loop/shared/img/02.png                        (content/shared/img/02.png)
   content/browser/loop/shared/img/02@2x.png                     (content/shared/img/02@2x.png)
   content/browser/loop/shared/img/telefonica.png                (content/shared/img/telefonica.png)
   content/browser/loop/shared/img/telefonica@2x.png             (content/shared/img/telefonica@2x.png)
 
   # 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/roomStore.js         (content/shared/js/roomStore.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/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)
+  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/roomStore.js           (content/shared/js/roomStore.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/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
   content/browser/loop/shared/libs/react-0.11.2.js    (content/shared/libs/react-0.11.2.js)
 #else
   content/browser/loop/shared/libs/react-0.11.2.js    (content/shared/libs/react-0.11.2-prod.js)
 #endif
   content/browser/loop/shared/libs/lodash-2.4.1.js    (content/shared/libs/lodash-2.4.1.js)
--- a/browser/components/loop/standalone/Makefile
+++ b/browser/components/loop/standalone/Makefile
@@ -78,12 +78,13 @@ config:
 	@echo "loop.config.feedbackApiUrl = '`echo $(LOOP_FEEDBACK_API_URL)`';" >> content/config.js
 	@echo "loop.config.feedbackProductName = '`echo $(LOOP_FEEDBACK_PRODUCT_NAME)`';" >> content/config.js
 	@echo "loop.config.brandWebsiteUrl = '`echo $(LOOP_BRAND_WEBSITE_URL)`';" >> content/config.js
 	@echo "loop.config.privacyWebsiteUrl = '`echo $(LOOP_PRIVACY_WEBSITE_URL)`';" >> content/config.js
 	@echo "loop.config.legalWebsiteUrl = '`echo $(LOOP_LEGAL_WEBSITE_URL)`';" >> content/config.js
 	@echo "loop.config.learnMoreUrl = '`echo $(LOOP_PRODUCT_HOMEPAGE_URL)`';" >> content/config.js
 	@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
 	@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
+	@echo "loop.config.fxosApp.rooms = true;" >> content/config.js
 	@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js
 	@echo "loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';" >> content/config.js
 	@echo "loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';" >> content/config.js
 	@echo "loop.config.generalSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';" >> content/config.js
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -93,22 +93,25 @@
     <script type="text/javascript" src="shared/js/views.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/feedbackViews.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>
     <script type="text/javascript" src="js/standaloneRoomViews.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
     <script>
       // Wait for all the localization notes to load
       window.addEventListener('localized', function() {
         loop.webapp.init();
       }, false);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/standalone/content/js/fxOSMarketplace.js
@@ -0,0 +1,37 @@
+/** @jsx React.DOM */
+
+var loop = loop || {};
+loop.fxOSMarketplaceViews = (function() {
+  "use strict";
+
+  /**
+   * The Firefox Marketplace exposes a web page that contains a postMesssage
+   * based API that wraps a small set of functionality from the WebApps API
+   * that allow us to request the installation of apps given their manifest
+   * URL. We will be embedding the content of this web page within an hidden
+   * iframe in case that we need to request the installation of the FxOS Loop
+   * client.
+   */
+  var FxOSHiddenMarketplaceView = React.createClass({displayName: 'FxOSHiddenMarketplaceView',
+    render: function() {
+      return React.DOM.iframe({id: "marketplace", src: this.props.marketplaceSrc, hidden: true});
+    },
+
+    componentDidUpdate: function() {
+      // This happens only once when we change the 'src' property of the iframe.
+      if (this.props.onMarketplaceMessage) {
+        // The reason for listening on the global window instead of on the
+        // iframe content window is because the Marketplace is doing a
+        // window.top.postMessage.
+        // XXX Bug 1097703: This should be changed to an action when the old
+        //     style URLs go away.
+        window.addEventListener("message", this.props.onMarketplaceMessage);
+      }
+    }
+  });
+
+  return {
+    FxOSHiddenMarketplaceView: FxOSHiddenMarketplaceView
+  };
+
+})();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/standalone/content/js/fxOSMarketplace.jsx
@@ -0,0 +1,37 @@
+/** @jsx React.DOM */
+
+var loop = loop || {};
+loop.fxOSMarketplaceViews = (function() {
+  "use strict";
+
+  /**
+   * The Firefox Marketplace exposes a web page that contains a postMesssage
+   * based API that wraps a small set of functionality from the WebApps API
+   * that allow us to request the installation of apps given their manifest
+   * URL. We will be embedding the content of this web page within an hidden
+   * iframe in case that we need to request the installation of the FxOS Loop
+   * client.
+   */
+  var FxOSHiddenMarketplaceView = React.createClass({
+    render: function() {
+      return <iframe id="marketplace" src={this.props.marketplaceSrc} hidden/>;
+    },
+
+    componentDidUpdate: function() {
+      // This happens only once when we change the 'src' property of the iframe.
+      if (this.props.onMarketplaceMessage) {
+        // The reason for listening on the global window instead of on the
+        // iframe content window is because the Marketplace is doing a
+        // window.top.postMessage.
+        // XXX Bug 1097703: This should be changed to an action when the old
+        //     style URLs go away.
+        window.addEventListener("message", this.props.onMarketplaceMessage);
+      }
+    }
+  });
+
+  return {
+    FxOSHiddenMarketplaceView: FxOSHiddenMarketplaceView
+  };
+
+})();
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -15,18 +15,20 @@ loop.standaloneRoomViews = (function(moz
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea',
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
-      activeRoomStore:
-        React.PropTypes.instanceOf(loop.store.ActiveRoomStore).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
     },
 
     onFeedbackSent: function() {
       // We pass a tick to prevent React warnings regarding nested updates.
       setTimeout(function() {
         this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
@@ -184,18 +186,20 @@ loop.standaloneRoomViews = (function(moz
 
   var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
     mixins: [
       Backbone.Events,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
-      activeRoomStore:
-        React.PropTypes.instanceOf(loop.store.ActiveRoomStore).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,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
@@ -374,16 +378,19 @@ loop.standaloneRoomViews = (function(moz
                 audio: {enabled: !this.state.audioMuted,
                         visible: this._roomIsActive()}, 
                 publishStream: this.publishStream, 
                 hangup: this.leaveRoom, 
                 hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), 
                 enableHangup: this._roomIsActive()})
             )
           ), 
+          loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView({
+            marketplaceSrc: this.state.marketplaceSrc, 
+            onMarketplaceMessage: this.state.onMarketplaceMessage}), 
           StandaloneRoomFooter(null)
         )
       );
     }
   });
 
   return {
     StandaloneRoomView: StandaloneRoomView
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -15,18 +15,20 @@ loop.standaloneRoomViews = (function(moz
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
-      activeRoomStore:
-        React.PropTypes.instanceOf(loop.store.ActiveRoomStore).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
     },
 
     onFeedbackSent: function() {
       // We pass a tick to prevent React warnings regarding nested updates.
       setTimeout(function() {
         this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
@@ -184,18 +186,20 @@ loop.standaloneRoomViews = (function(moz
 
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
       sharedMixins.RoomsAudioMixin
     ],
 
     propTypes: {
-      activeRoomStore:
-        React.PropTypes.instanceOf(loop.store.ActiveRoomStore).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,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
     },
 
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
@@ -374,16 +378,19 @@ loop.standaloneRoomViews = (function(moz
                 audio={{enabled: !this.state.audioMuted,
                         visible: this._roomIsActive()}}
                 publishStream={this.publishStream}
                 hangup={this.leaveRoom}
                 hangupButtonLabel={mozL10n.get("rooms_leave_button_label")}
                 enableHangup={this._roomIsActive()} />
             </div>
           </div>
+          <loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
+            marketplaceSrc={this.state.marketplaceSrc}
+            onMarketplaceMessage={this.state.onMarketplaceMessage} />
           <StandaloneRoomFooter />
         </div>
       );
     }
   });
 
   return {
     StandaloneRoomView: StandaloneRoomView
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -122,52 +122,31 @@ loop.webapp = (function($, _, OT, mozL10
       return (
         React.DOM.h1({className: "standalone-header-title"}, 
           React.DOM.strong(null, mozL10n.get("clientShortname2"))
         )
       );
     }
   });
 
-  /**
-   * The Firefox Marketplace exposes a web page that contains a postMesssage
-   * based API that wraps a small set of functionality from the WebApps API
-   * that allow us to request the installation of apps given their manifest
-   * URL. We will be embedding the content of this web page within an hidden
-   * iframe in case that we need to request the installation of the FxOS Loop
-   * client.
-   */
-  var FxOSHiddenMarketplace = React.createClass({displayName: 'FxOSHiddenMarketplace',
-    render: function() {
-      return React.DOM.iframe({id: "marketplace", src: this.props.marketplaceSrc, hidden: true});
-    },
-
-    componentDidUpdate: function() {
-      // This happens only once when we change the 'src' property of the iframe.
-      if (this.props.onMarketplaceMessage) {
-        // The reason for listening on the global window instead of on the
-        // iframe content window is because the Marketplace is doing a
-        // window.top.postMessage.
-        window.addEventListener("message", this.props.onMarketplaceMessage);
+  var FxOSConversationModel = Backbone.Model.extend({
+    setupOutgoingCall: function(selectedCallType) {
+      if (selectedCallType) {
+        this.set("selectedCallType", selectedCallType);
       }
-    }
-  });
-
-  var FxOSConversationModel = Backbone.Model.extend({
-    setupOutgoingCall: function() {
       // The FxOS Loop client exposes a "loop-call" activity. If we get the
       // activity onerror callback it means that there is no "loop-call"
       // activity handler available and so no FxOS Loop client installed.
       var request = new MozActivity({
         name: "loop-call",
         data: {
           type: "loop/token",
           token: this.get("loopToken"),
           callerId: this.get("callerId"),
-          callType: this.get("callType")
+          video: this.get("selectedCallType") === "audio-video"
         }
       });
 
       request.onsuccess = function() {};
 
       request.onerror = (function(event) {
         if (event.target.error.name !== "NO_PROVIDER") {
           console.error ("Unexpected " + event.target.error.name);
@@ -560,17 +539,17 @@ loop.webapp = (function($, _, OT, mozL10
               ), 
               React.DOM.div({className: "flex-padding-1"})
             ), 
 
             React.DOM.p({className: tosClasses, 
                dangerouslySetInnerHTML: {__html: tosHTML}})
           ), 
 
-          FxOSHiddenMarketplace({
+          loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView({
             marketplaceSrc: this.state.marketplaceSrc, 
             onMarketplaceMessage: this.state.onMarketplaceMessage}), 
 
           ConversationFooter(null)
         )
       );
     }
   });
@@ -954,18 +933,20 @@ loop.webapp = (function($, _, OT, mozL10
       helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for flux style
       standaloneAppStore: React.PropTypes.instanceOf(
         loop.store.StandaloneAppStore).isRequired,
-      activeRoomStore: React.PropTypes.instanceOf(
-        loop.store.ActiveRoomStore).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)
     },
 
     getInitialState: function() {
       return this.props.standaloneAppStore.getStoreState();
     },
 
@@ -1031,24 +1012,16 @@ loop.webapp = (function($, _, OT, mozL10
   function init() {
     var helper = new sharedUtils.Helper();
     var standaloneMozLoop = new loop.StandaloneMozLoop({
       baseServerUrl: loop.config.serverUrl
     });
 
     // Older non-flux based items.
     var notifications = new sharedModels.NotificationCollection();
-    var conversation
-    if (helper.isFirefoxOS(navigator.userAgent)) {
-      conversation = new FxOSConversationModel();
-    } else {
-      conversation = new sharedModels.ConversationModel({}, {
-        sdk: OT
-      });
-    }
 
     var feedbackApiClient = new loop.FeedbackAPIClient(
       loop.config.feedbackApiUrl, {
         product: loop.config.feedbackProductName,
         user_agent: navigator.userAgent,
         url: document.location.origin
       });
 
@@ -1056,34 +1029,53 @@ loop.webapp = (function($, _, OT, mozL10
     var dispatcher = new loop.Dispatcher();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var sdkDriver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
       sdk: OT
     });
+    var conversation;
+    var activeRoomStore;
+    if (helper.isFirefoxOS(navigator.userAgent)) {
+      if (loop.config.fxosApp) {
+        conversation = new FxOSConversationModel();
+        if (loop.config.fxosApp.rooms) {
+          activeRoomStore = new loop.store.FxOSActiveRoomStore(dispatcher, {
+          mozLoop: standaloneMozLoop
+          });
+        }
+      }
+    }
+
+    conversation = conversation ||
+      new sharedModels.ConversationModel({}, {
+        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,
       helper: helper,
       sdk: OT
     });
-    var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
-      mozLoop: standaloneMozLoop,
-      sdkDriver: sdkDriver
-    });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
     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
@@ -122,52 +122,31 @@ loop.webapp = (function($, _, OT, mozL10
       return (
         <h1 className="standalone-header-title">
           <strong>{mozL10n.get("clientShortname2")}</strong>
         </h1>
       );
     }
   });
 
-  /**
-   * The Firefox Marketplace exposes a web page that contains a postMesssage
-   * based API that wraps a small set of functionality from the WebApps API
-   * that allow us to request the installation of apps given their manifest
-   * URL. We will be embedding the content of this web page within an hidden
-   * iframe in case that we need to request the installation of the FxOS Loop
-   * client.
-   */
-  var FxOSHiddenMarketplace = React.createClass({
-    render: function() {
-      return <iframe id="marketplace" src={this.props.marketplaceSrc} hidden/>;
-    },
-
-    componentDidUpdate: function() {
-      // This happens only once when we change the 'src' property of the iframe.
-      if (this.props.onMarketplaceMessage) {
-        // The reason for listening on the global window instead of on the
-        // iframe content window is because the Marketplace is doing a
-        // window.top.postMessage.
-        window.addEventListener("message", this.props.onMarketplaceMessage);
+  var FxOSConversationModel = Backbone.Model.extend({
+    setupOutgoingCall: function(selectedCallType) {
+      if (selectedCallType) {
+        this.set("selectedCallType", selectedCallType);
       }
-    }
-  });
-
-  var FxOSConversationModel = Backbone.Model.extend({
-    setupOutgoingCall: function() {
       // The FxOS Loop client exposes a "loop-call" activity. If we get the
       // activity onerror callback it means that there is no "loop-call"
       // activity handler available and so no FxOS Loop client installed.
       var request = new MozActivity({
         name: "loop-call",
         data: {
           type: "loop/token",
           token: this.get("loopToken"),
           callerId: this.get("callerId"),
-          callType: this.get("callType")
+          video: this.get("selectedCallType") === "audio-video"
         }
       });
 
       request.onsuccess = function() {};
 
       request.onerror = (function(event) {
         if (event.target.error.name !== "NO_PROVIDER") {
           console.error ("Unexpected " + event.target.error.name);
@@ -560,17 +539,17 @@ loop.webapp = (function($, _, OT, mozL10
               />
               <div className="flex-padding-1" />
             </div>
 
             <p className={tosClasses}
                dangerouslySetInnerHTML={{__html: tosHTML}}></p>
           </div>
 
-          <FxOSHiddenMarketplace
+          <loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
             marketplaceSrc={this.state.marketplaceSrc}
             onMarketplaceMessage= {this.state.onMarketplaceMessage} />
 
           <ConversationFooter />
         </div>
       );
     }
   });
@@ -954,18 +933,20 @@ loop.webapp = (function($, _, OT, mozL10
       helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
                           .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for flux style
       standaloneAppStore: React.PropTypes.instanceOf(
         loop.store.StandaloneAppStore).isRequired,
-      activeRoomStore: React.PropTypes.instanceOf(
-        loop.store.ActiveRoomStore).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)
     },
 
     getInitialState: function() {
       return this.props.standaloneAppStore.getStoreState();
     },
 
@@ -1031,24 +1012,16 @@ loop.webapp = (function($, _, OT, mozL10
   function init() {
     var helper = new sharedUtils.Helper();
     var standaloneMozLoop = new loop.StandaloneMozLoop({
       baseServerUrl: loop.config.serverUrl
     });
 
     // Older non-flux based items.
     var notifications = new sharedModels.NotificationCollection();
-    var conversation
-    if (helper.isFirefoxOS(navigator.userAgent)) {
-      conversation = new FxOSConversationModel();
-    } else {
-      conversation = new sharedModels.ConversationModel({}, {
-        sdk: OT
-      });
-    }
 
     var feedbackApiClient = new loop.FeedbackAPIClient(
       loop.config.feedbackApiUrl, {
         product: loop.config.feedbackProductName,
         user_agent: navigator.userAgent,
         url: document.location.origin
       });
 
@@ -1056,34 +1029,53 @@ loop.webapp = (function($, _, OT, mozL10
     var dispatcher = new loop.Dispatcher();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var sdkDriver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
       sdk: OT
     });
+    var conversation;
+    var activeRoomStore;
+    if (helper.isFirefoxOS(navigator.userAgent)) {
+      if (loop.config.fxosApp) {
+        conversation = new FxOSConversationModel();
+        if (loop.config.fxosApp.rooms) {
+          activeRoomStore = new loop.store.FxOSActiveRoomStore(dispatcher, {
+          mozLoop: standaloneMozLoop
+          });
+        }
+      }
+    }
+
+    conversation = conversation ||
+      new sharedModels.ConversationModel({}, {
+        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,
       helper: helper,
       sdk: OT
     });
-    var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
-      mozLoop: standaloneMozLoop,
-      sdkDriver: sdkDriver
-    });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
     window.addEventListener("unload", function() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
--- a/browser/components/loop/standalone/server.js
+++ b/browser/components/loop/standalone/server.js
@@ -25,16 +25,17 @@ function getConfigFile(req, res) {
     //     uploaded to the marketplace bug 1053424
     "loop.config.marketplaceUrl = 'http://fake-market.herokuapp.com/iframe-install.html'",
     "loop.config.brandWebsiteUrl = 'https://www.mozilla.org/firefox/';",
     "loop.config.privacyWebsiteUrl = 'https://www.mozilla.org/privacy/firefox-hello/';",
     "loop.config.learnMoreUrl = 'https://www.mozilla.org/hello/';",
     "loop.config.legalWebsiteUrl = 'https://www.mozilla.org/about/legal/terms/firefox-hello/';",
     "loop.config.fxosApp = loop.config.fxosApp || {};",
     "loop.config.fxosApp.name = 'Loop';",
+    "loop.config.fxosApp.rooms = true;",
     "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';",
     "loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';",
     "loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';",
     "loop.config.generalSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';"
   ].join("\n"));
 }
 
 app.get('/content/config.js', getConfigFile);
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -47,16 +47,18 @@
   <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/roomStore.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/feedbackViews.js"></script>
   <script src="../../content/js/client.js"></script>
   <script src="../../content/js/conversationAppStore.js"></script>
   <script src="../../content/js/roomViews.js"></script>
   <script src="../../content/js/conversationViews.js"></script>
   <script src="../../content/js/conversation.js"></script>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -941,21 +941,25 @@ describe("loop.panel", function() {
         TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
 
         sinon.assert.calledWith(dispatch, new sharedActions.CreateRoom({
           nameTemplate: "fakeText",
           roomOwner: fakeEmail
         }));
       });
 
-    it("should close the panel when 'Start a Conversation' is clicked",
+    it("should close the panel once a room is created and there is no error",
       function() {
         var view = createTestComponent();
 
-        TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
+        roomStore.setStoreState({pendingCreation: true});
+
+        sinon.assert.notCalled(fakeWindow.close);
+
+        roomStore.setStoreState({pendingCreation: false});
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
 
     it("should disable the create button when a creation operation is ongoing",
       function() {
         roomStore.setStoreState({pendingCreation: true});
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/fxOSActiveRoomStore_test.js
@@ -0,0 +1,212 @@
+/* global chai, loop */
+
+var expect = chai.expect;
+var sharedActions = loop.shared.actions;
+
+describe("loop.store.FxOSActiveRoomStore", function () {
+  "use strict";
+
+  var ROOM_STATES = loop.store.ROOM_STATES;
+
+  var sandbox;
+  var dispatcher;
+  var fakeMozLoop;
+  var store;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    sandbox.useFakeTimers();
+
+    dispatcher = new loop.Dispatcher();
+    sandbox.stub(dispatcher, "dispatch");
+
+    fakeMozLoop = {
+      setLoopPref: sandbox.stub(),
+      rooms: {
+        join: sinon.stub()
+      }
+    };
+
+    store = new loop.store.FxOSActiveRoomStore(dispatcher, {
+      mozLoop: fakeMozLoop
+    });
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("#FxOSActiveRoomStore - constructor", function() {
+    it("should throw an error if mozLoop is missing", function() {
+      expect(function() {
+        new loop.store.FxOSActiveRoomStore(dispatcher);
+      }).to.Throw(/mozLoop/);
+    });
+  });
+
+  describe("#FxOSActiveRoomStore - fetchServerData", function() {
+    it("should save the token", function() {
+      store.fetchServerData(new sharedActions.FetchServerData({
+        windowType: "room",
+        token: "fakeToken"
+      }));
+
+      expect(store.getStoreState().roomToken).eql("fakeToken");
+    });
+
+    it("should set the state to `READY`", function() {
+      store.fetchServerData(new sharedActions.FetchServerData({
+        windowType: "room",
+        token: "fakeToken"
+      }));
+
+      expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
+    });
+  });
+
+  describe("#FxOSActiveRoomStore - setupOutgoingRoom", function() {
+    var realMozActivity;
+    var _activityDetail;
+    var _onerror;
+
+    function fireError(errorName) {
+      _onerror({
+        target: {
+          error: {
+            name: errorName
+          }
+        }
+      });
+    }
+
+    before(function() {
+      realMozActivity = window.MozActivity;
+
+      window.MozActivity = function(activityDetail) {
+        _activityDetail = activityDetail;
+        return {
+          set onerror(cbk) {
+            _onerror = cbk;
+          }
+        };
+      };
+    });
+
+    after(function() {
+      window.MozActivity = realMozActivity;
+    });
+
+    beforeEach(function() {
+      sandbox.stub(console, "error");
+      _activityDetail = undefined;
+      _onerror = undefined;
+    });
+
+    afterEach(function() {
+      sandbox.restore();
+    });
+
+    it("should reset failureReason", function() {
+      store.setStoreState({failureReason: "Test"});
+
+      store.joinRoom();
+
+      expect(store.getStoreState().failureReason).eql(undefined);
+    });
+
+    it("should create an activity", function() {
+      store.setStoreState({
+        roomToken: "fakeToken",
+        token: "fakeToken"
+      });
+
+      expect(_activityDetail).to.not.exist;
+      store.joinRoom();
+      expect(_activityDetail).to.exist;
+      expect(_activityDetail).eql({
+        name: "room-call",
+        data: {
+          type: "loop/rToken",
+          token: "fakeToken"
+        }
+      });
+    });
+
+    it("should change the store state when the activity fail with a " +
+       "NO_PROVIDER error", function() {
+
+      loop.config = {
+        marketplaceUrl: "http://market/"
+      };
+      store._setupOutgoingRoom(true);
+      fireError("NO_PROVIDER");
+      expect(store.getStoreState().marketplaceSrc).eql(
+        loop.config.marketplaceUrl
+      );
+    });
+
+    it("should log an error when the activity fail with a error different " +
+       "from NO_PROVIDER", function() {
+      loop.config = {
+        marketplaceUrl: "http://market/"
+      };
+      store._setupOutgoingRoom(true);
+      fireError("whatever");
+      sinon.assert.calledOnce(console.error);
+      sinon.assert.calledWith(console.error, "Unexpected whatever");
+    });
+
+    it("should log an error and exist if an activity error is received when " +
+       "the parameter is false ", function() {
+      loop.config = {
+        marketplaceUrl: "http://market/"
+      };
+      store._setupOutgoingRoom(false);
+      fireError("whatever");
+      sinon.assert.calledOnce(console.error);
+      sinon.assert.calledWith(console.error,
+        "Unexpected activity launch error after the app has been installed");
+    });
+  });
+
+  describe("#FxOSActiveRoomStore - _onMarketplaceMessage", function() {
+    var setupOutgoingRoom;
+
+    beforeEach(function() {
+      sandbox.stub(console, "error");
+      setupOutgoingRoom = sandbox.stub(store, "_setupOutgoingRoom");
+    });
+
+    afterEach(function() {
+      setupOutgoingRoom.restore();
+    });
+
+    it("We should call trigger a FxOS outgoing call if we get " +
+       "install-package message without error", function() {
+
+      sinon.assert.notCalled(setupOutgoingRoom);
+      store._onMarketplaceMessage({
+        data: {
+          name: "install-package"
+        }
+      });
+      sinon.assert.calledOnce(setupOutgoingRoom);
+    });
+
+    it("We should log an error if we get install-package message with an " +
+       "error", function() {
+
+      sinon.assert.notCalled(setupOutgoingRoom);
+      store._onMarketplaceMessage({
+        data: {
+          name: "install-package",
+          error: { error: "whatever error" }
+        }
+      });
+      sinon.assert.notCalled(setupOutgoingRoom);
+      sinon.assert.calledOnce(console.error);
+      sinon.assert.calledWith(console.error, "whatever error");
+    });
+  });
+});
+
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -45,16 +45,18 @@
   <script src="../../content/shared/js/views.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/roomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
   <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/feedbackViews.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
@@ -62,16 +64,17 @@
   <script src="utils_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="roomStore_test.js"></script>
   <script>
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
--- a/browser/components/loop/test/shared/roomStore_test.js
+++ b/browser/components/loop/test/shared/roomStore_test.js
@@ -59,17 +59,17 @@ describe("loop.store.RoomStore", functio
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
         new loop.store.RoomStore(dispatcher);
       }).to.Throw(/mozLoop/);
     });
   });
 
   describe("constructed", function() {
-    var fakeMozLoop, store;
+    var fakeMozLoop, fakeNotifications, store;
 
     var defaultStoreState = {
       error: undefined,
       pendingCreation: false,
       pendingInitialRetrieval: false,
       rooms: [],
       activeRoom: {}
     };
@@ -81,17 +81,24 @@ describe("loop.store.RoomStore", functio
         rooms: {
           create: function() {},
           getAll: function() {},
           open: function() {},
           rename: function() {},
           on: sandbox.stub()
         }
       };
-      store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
+      fakeNotifications = {
+        set: sinon.stub(),
+        remove: sinon.stub()
+      };
+      store = new loop.store.RoomStore(dispatcher, {
+        mozLoop: fakeMozLoop,
+        notifications: fakeNotifications
+      });
       store.setStoreState(defaultStoreState);
     });
 
     describe("MozLoop rooms event listeners", function() {
       beforeEach(function() {
         _.extend(fakeMozLoop.rooms, Backbone.Events);
 
         fakeMozLoop.rooms.getAll = function(version, cb) {
@@ -148,17 +155,17 @@ describe("loop.store.RoomStore", functio
       });
 
       describe("refresh", function() {
         it ("should clear the list of rooms", function() {
           fakeMozLoop.rooms.trigger("refresh", "refresh");
 
           expect(store.getStoreState().rooms).to.have.length.of(0);
         });
-      })
+      });
     });
 
     describe("#findNextAvailableRoomNumber", function() {
       var fakeNameTemplate = "RoomWord {{conversationLabel}}";
 
       it("should find next available room number from an empty room list",
         function() {
           store.setStoreState({rooms: []});
@@ -198,78 +205,136 @@ describe("loop.store.RoomStore", functio
         roomToken: "fake",
         roomUrl: "http://invalid",
         maxSize: 42,
         participants: [],
         ctime: 1234567890
       };
 
       beforeEach(function() {
+        sandbox.stub(dispatcher, "dispatch");
         store.setStoreState({pendingCreation: false, rooms: []});
       });
 
+      it("should clear any existing room errors", function() {
+        sandbox.stub(fakeMozLoop.rooms, "create");
+
+        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+        sinon.assert.calledOnce(fakeNotifications.remove);
+        sinon.assert.calledWithExactly(fakeNotifications.remove,
+          "create-room-error");
+      });
+
       it("should request creation of a new room", function() {
         sandbox.stub(fakeMozLoop.rooms, "create");
 
         store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
 
         sinon.assert.calledWith(fakeMozLoop.rooms.create, {
           roomName: "Conversation 1",
           roomOwner: fakeOwner,
           maxSize: store.maxRoomCreationSize,
           expiresIn: store.defaultExpiresIn
         });
       });
 
-      it("should store any creation encountered error", function() {
-        var err = new Error("fake");
-        sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
-          cb(err);
-        });
-
-        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
-
-        expect(store.getStoreState().error).eql(err);
-      });
-
       it("should switch the pendingCreation state flag to true", function() {
         sandbox.stub(fakeMozLoop.rooms, "create");
 
         store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
 
         expect(store.getStoreState().pendingCreation).eql(true);
       });
 
-      it("should switch the pendingCreation state flag to false once the " +
-         "operation is done", function() {
-        sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
-          cb(null, {roomToken: "fakeToken"});
+      it("should dispatch a CreatedRoom action once the operation is done",
+        function() {
+          sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
+            cb(null, {roomToken: "fakeToken"});
+          });
+
+          store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.CreatedRoom({
+              roomToken: "fakeToken"
+            }));
         });
 
-        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+      it("should dispatch a CreateRoomError action if the operation fails",
+        function() {
+          var err = new Error("fake");
+          sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
+            cb(err);
+          });
+
+          store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.CreateRoomError({
+              error: err
+            }));
+        });
+   });
+
+   describe("#createdRoom", function() {
+      beforeEach(function() {
+        sandbox.stub(dispatcher, "dispatch");
+      });
+
+      it("should switch the pendingCreation state flag to false", function() {
+        store.setStoreState({pendingCreation:true});
+
+        store.createdRoom(new sharedActions.CreatedRoom({
+          roomToken: "fakeToken"
+        }));
 
         expect(store.getStoreState().pendingCreation).eql(false);
       });
 
       it("should dispatch an OpenRoom action once the operation is done",
         function() {
-          var dispatch = sandbox.stub(dispatcher, "dispatch");
-          sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
-            cb(null, {roomToken: "fakeToken"});
-          });
-
-          store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
-
-          sinon.assert.calledOnce(dispatch);
-          sinon.assert.calledWithExactly(dispatch, new sharedActions.OpenRoom({
+          store.createdRoom(new sharedActions.CreatedRoom({
             roomToken: "fakeToken"
           }));
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.OpenRoom({
+              roomToken: "fakeToken"
+            }));
         });
     });
 
+    describe("#createRoomError", function() {
+      it("should switch the pendingCreation state flag to false", function() {
+        store.setStoreState({pendingCreation:true});
+
+        store.createRoomError({
+          error: new Error("fake")
+        });
+
+        expect(store.getStoreState().pendingCreation).eql(false);
+      });
+
+      it("should set a notification", function() {
+        store.createRoomError({
+          error: new Error("fake")
+        });
+
+        sinon.assert.calledOnce(fakeNotifications.set);
+        sinon.assert.calledWithMatch(fakeNotifications.set, {
+          id: "create-room-error",
+          level: "error"
+        });
+      });
+    });
+
     describe("#copyRoomUrl", function() {
       it("should copy the room URL", function() {
         var copyString = sandbox.stub(fakeMozLoop, "copyString");
 
         store.copyRoomUrl(new sharedActions.CopyRoomUrl({
           roomUrl: "http://invalid"
         }));
 
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -43,24 +43,27 @@
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/views.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/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/webapp.js"></script>
   <!-- Test scripts -->
   <script src="standalone_client_test.js"></script>
   <script src="standaloneAppStore_test.js"></script>
   <script src="standaloneMozLoop_test.js"></script>
   <script src="standaloneRoomViews_test.js"></script>
   <script src="webapp_test.js"></script>
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -327,11 +327,31 @@ describe("loop.standaloneRoomViews", fun
               roomState: ROOM_STATES.SESSION_CONNECTED,
               videoMuted: true
             });
 
             expect(view.getDOMNode().querySelector(".local-stream-audio"))
               .not.eql(null);
           });
       });
+
+      describe("Marketplace hidden iframe", function() {
+
+        it("should set src when the store state change",
+           function(done) {
+
+          var marketplace = view.getDOMNode().querySelector("#marketplace");
+          expect(marketplace.src).to.be.equal("");
+
+          activeRoomStore.setStoreState({
+            marketplaceSrc: "http://market/",
+            onMarketplaceMessage: function () {}
+          });
+
+          view.forceUpdate(function() {
+            expect(marketplace.src).to.be.equal("http://market/");
+            done();
+          });
+        });
+      });
     });
   });
 });
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -1164,18 +1164,17 @@ describe("loop.webapp", function() {
     });
 
     describe("FxOSConversationModel", function() {
       var model, realMozActivity;
 
       before(function() {
         model = new loop.webapp.FxOSConversationModel({
           loopToken: "fakeToken",
-          callerId: "callerId",
-          callType: "callType"
+          callerId: "callerId"
         });
 
         realMozActivity = window.MozActivity;
 
         loop.config.fxosApp = {
           name: "Firefox Hello"
         };
       });
@@ -1209,33 +1208,64 @@ describe("loop.webapp", function() {
         });
 
         after(function() {
           window.MozActivity = realMozActivity;
         });
 
         beforeEach(function() {
           trigger = sandbox.stub(model, "trigger");
+          _activityProps = undefined;
         });
 
         afterEach(function() {
           trigger.restore();
         });
 
-        it("Activity properties", function() {
+        it("Activity properties with video call", function() {
+          expect(_activityProps).to.not.exist;
+          model.setupOutgoingCall("audio-video");
+          expect(_activityProps).to.exist;
+          expect(_activityProps).eql({
+            name: "loop-call",
+            data: {
+              type: "loop/token",
+              token: "fakeToken",
+              callerId: "callerId",
+              video: true
+            }
+          });
+        });
+
+        it("Activity properties with audio call", function() {
+          expect(_activityProps).to.not.exist;
+          model.setupOutgoingCall("audio");
+          expect(_activityProps).to.exist;
+          expect(_activityProps).eql({
+            name: "loop-call",
+            data: {
+              type: "loop/token",
+              token: "fakeToken",
+              callerId: "callerId",
+              video: false
+            }
+          });
+        });
+
+        it("Activity properties by default", function() {
           expect(_activityProps).to.not.exist;
           model.setupOutgoingCall();
           expect(_activityProps).to.exist;
           expect(_activityProps).eql({
             name: "loop-call",
             data: {
               type: "loop/token",
               token: "fakeToken",
               callerId: "callerId",
-              callType: "callType"
+              video: false
             }
           });
         });
 
         it("NO_PROVIDER activity error should trigger fxos:app-needed",
           function() {
             sinon.assert.notCalled(trigger);
             model.setupOutgoingCall();
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -39,22 +39,25 @@
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/views.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/roomStore.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/feedbackViews.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="../content/js/fxOSMarketplace.js"></script>
     <script src="../content/js/webapp.js"></script>
     <script src="../content/js/standaloneRoomViews.js"></script>
     <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
     <script>
       if (!loop.contacts) {
         // For browsers that don't support ES6 without special flags (all but Fx
         // at the moment), we shim the contacts namespace with its most barebone
         // implementation.
--- a/layout/inspector/inDOMUtils.cpp
+++ b/layout/inspector/inDOMUtils.cpp
@@ -862,16 +862,21 @@ inDOMUtils::CssPropertyIsValid(const nsA
   nsCSSProperty propertyID =
     nsCSSProps::LookupProperty(aPropertyName, nsCSSProps::eIgnoreEnabledState);
 
   if (propertyID == eCSSProperty_UNKNOWN) {
     *_retval = false;
     return NS_OK;
   }
 
+  if (propertyID == eCSSPropertyExtra_variable) {
+    *_retval = true;
+    return NS_OK;
+  }
+
   // Get a parser, parse the property.
   nsCSSParser parser;
   *_retval = parser.IsValueValidForProperty(propertyID, aPropertyValue);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/layout/inspector/tests/test_css_property_is_valid.html
+++ b/layout/inspector/tests/test_css_property_is_valid.html
@@ -69,16 +69,21 @@
       property: "color",
       value: "red; background:green;",
       expected: false
     },
     {
       property: "content",
       value: "\"hello\"",
       expected: true
+    },
+    {
+      property: "color",
+      value: "var(--some-kind-of-green)",
+      expected: true
     }
   ];
 
   for (let {property, value, expected} of tests) {
     let valid = utils.cssPropertyIsValid(property, value);
 
     if (expected) {
       ok(valid, property + ":" + value + " is valid");
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -344,16 +344,18 @@ var BrowserApp = {
 #ifdef MOZ_SAFE_BROWSING
         Services.tm.mainThread.dispatch(function() {
           // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
           SafeBrowsing.init();
         }, Ci.nsIThread.DISPATCH_NORMAL);
 #endif
 #ifdef NIGHTLY_BUILD
         WebcompatReporter.init();
+        Telemetry.addData("TRACKING_PROTECTION_ENABLED",
+          Services.prefs.getBoolPref("privacy.trackingprotection.enabled"));
 #endif
       } catch(ex) { console.log(ex); }
     }, false);
 
     BrowserEventHandler.init();
     ViewportHandler.init();
 
     Services.androidBridge.browserApp = this;
@@ -6783,25 +6785,28 @@ var IdentityHandler = {
       return this.MIXED_MODE_CONTENT_LOADED;
     }
 
     return this.MIXED_MODE_UNKNOWN;
   },
 
   getTrackingMode: function getTrackingMode(aState) {
     if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
+      Telemetry.addData("TRACKING_PROTECTION_SHIELD", 2);
       return this.TRACKING_MODE_CONTENT_BLOCKED;
     }
 
     // Only show an indicator for loaded tracking content if the pref to block it is enabled
     if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) &&
          Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
+      Telemetry.addData("TRACKING_PROTECTION_SHIELD", 1);
       return this.TRACKING_MODE_CONTENT_LOADED;
     }
 
+    Telemetry.addData("TRACKING_PROTECTION_SHIELD", 0);
     return this.TRACKING_MODE_UNKNOWN;
   },
 
   /**
    * Determine the identity of the page being displayed by examining its SSL cert
    * (if available). Return the data needed to update the UI.
    */
   checkIdentity: function checkIdentity(aState, aBrowser) {
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -249,26 +249,38 @@ this.Download.prototype = {
    * updated regardless of the value of hasProgress.
    */
   speed: 0,
 
   /**
    * Indicates whether, at this time, there is any partially downloaded data
    * that can be used when restarting a failed or canceled download.
    *
+   * Even if the download has partial data on disk, hasPartialData will be false
+   * if that data cannot be used to restart the download. In order to determine
+   * if a part file is being used which contains partial data the
+   * Download.target.partFilePath should be checked.
+   *
    * This property is relevant while the download is in progress, and also if it
    * failed or has been canceled.  If the download has been completed
    * successfully, this property is always false.
    *
    * Whether partial data can actually be retained depends on the saver and the
    * download source, and may not be known before the download is started.
    */
   hasPartialData: false,
 
   /**
+   * Indicates whether, at this time, there is any data that has been blocked.
+   * Since reputation blocking takes place after the download has fully
+   * completed a value of true also indicates 100% of the data is present.
+   */
+  hasBlockedData: false,
+
+  /**
    * This can be set to a function that is called after other properties change.
    */
   onchange: null,
 
   /**
    * This tells if the user has chosen to open/run the downloaded file after
    * download has completed.
    */
@@ -348,21 +360,28 @@ this.Download.prototype = {
 
     // While shutting down or disposing of this object, we prevent the download
     // from returning to be in progress.
     if (this._finalized) {
       return Promise.reject(new DownloadError({
                                 message: "Cannot start after finalization."}));
     }
 
+    if (this.error && this.error.becauseBlockedByReputationCheck) {
+      return Promise.reject(new DownloadError({
+                                message: "Cannot start after being blocked " +
+                                         "by a reputation check."}));
+    }
+
     // Initialize all the status properties for a new or restarted download.
     this.stopped = false;
     this.canceled = false;
     this.error = null;
     this.hasProgress = false;
+    this.hasBlockedData = false;
     this.progress = 0;
     this.totalBytes = 0;
     this.currentBytes = 0;
     this.startTime = new Date();
 
     // Create a new deferred object and an associated promise before starting
     // the actual download.  We store it on the download as the current attempt.
     let deferAttempt = Promise.defer();
@@ -443,30 +462,26 @@ this.Download.prototype = {
           throw undefined;
         }
 
         // Execute the actual download through the saver object.
         this._saverExecuting = true;
         yield this.saver.execute(DS_setProgressBytes.bind(this),
                                  DS_setProperties.bind(this));
 
-        // Check for application reputation, which requires the entire file to
-        // be downloaded.  After that, check for the last time if the download
-        // has been canceled.  Both cases require the target file to be deleted,
-        // thus we process both in the same block of code.
-        if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) ||
-            this._promiseCanceled) {
+        // Check for the last time if the download has been canceled.
+        if (this._promiseCanceled) {
           try {
             yield OS.File.remove(this.target.path);
           } catch (ex) {
             Cu.reportError(ex);
           }
-          // If this is actually a cancellation, this exception will be changed
-          // in the catch block below.
-          throw new DownloadError({ becauseBlockedByReputationCheck: true });
+
+          // Cancellation exceptions will be changed in the catch block below.
+          throw new DownloadError();
         }
 
         // Update the status properties for a successful download.
         this.progress = 100;
         this.succeeded = true;
         this.hasPartialData = false;
       } catch (ex) {
         // Fail with a generic status code on cancellation, so that the caller
@@ -508,44 +523,154 @@ this.Download.prototype = {
 
         // Update the status properties, unless a new attempt already started.
         if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
           this._currentAttempt = null;
           this.stopped = true;
           this.speed = 0;
           this._notifyChange();
           if (this.succeeded) {
-            yield DownloadIntegration.downloadDone(this);
-
-            this._deferSucceeded.resolve();
-
-            if (this.launchWhenSucceeded) {
-              this.launch().then(null, Cu.reportError);
-
-              // Always schedule files to be deleted at the end of the private browsing
-              // mode, regardless of the value of the pref.
-              if (this.source.isPrivate) {
-                gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
-                                     new FileUtils.File(this.target.path));
-              } else if (Services.prefs.getBoolPref(
-                          "browser.helperApps.deleteTempFileOnExit")) {
-                gExternalAppLauncher.deleteTemporaryFileOnExit(
-                                     new FileUtils.File(this.target.path));
-              }
-            }
+            yield this._succeed();
           }
         }
       }
     }.bind(this)));
 
     // Notify the new download state before returning.
     this._notifyChange();
     return currentAttempt;
   },
 
+  /**
+   * Perform the actions necessary when a Download succeeds.
+   *
+   * @return {Promise}
+   * @resolves When the steps to take after success have completed.
+   * @rejects  JavaScript exception if any of the operations failed.
+   */
+  _succeed: Task.async(function* () {
+    yield DownloadIntegration.downloadDone(this);
+
+    this._deferSucceeded.resolve();
+
+    if (this.launchWhenSucceeded) {
+      this.launch().then(null, Cu.reportError);
+
+      // Always schedule files to be deleted at the end of the private browsing
+      // mode, regardless of the value of the pref.
+      if (this.source.isPrivate) {
+        gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
+                             new FileUtils.File(this.target.path));
+      } else if (Services.prefs.getBoolPref(
+                  "browser.helperApps.deleteTempFileOnExit")) {
+        gExternalAppLauncher.deleteTemporaryFileOnExit(
+                             new FileUtils.File(this.target.path));
+      }
+    }
+  }),
+
+  /**
+   * When a request to unblock the download is received, contains a promise
+   * that will be resolved when the unblock request is completed. This property
+   * will then continue to hold the promise indefinitely.
+   */
+  _promiseUnblock: null,
+
+  /**
+   * When a request to confirm the block of the download is received, contains
+   * a promise that will be resolved when cleaning up the download has
+   * completed. This property will then continue to hold the promise
+   * indefinitely.
+   */
+  _promiseConfirmBlock: null,
+
+  /**
+   * Unblocks a download which had been blocked by reputation.
+   *
+   * The file will be moved out of quarantine and the download will be
+   * marked as succeeded.
+   *
+   * @return {Promise}
+   * @resolves When the Download has been unblocked and succeeded.
+   * @rejects  JavaScript exception if any of the operations failed.
+   */
+  unblock: function() {
+    if (this._promiseUnblock) {
+      return this._promiseUnblock;
+    }
+
+    if (this._promiseConfirmBlock) {
+      return Promise.reject(new Error(
+        "Download block has been confirmed, cannot unblock."));
+    }
+
+    if (!this.hasBlockedData) {
+      return Promise.reject(new Error(
+        "unblock may only be called on Downloads with blocked data."));
+    }
+
+    this._promiseUnblock = Task.spawn(function* () {
+      try {
+        yield OS.File.move(this.target.partFilePath, this.target.path);
+      } catch (ex) {
+        yield this.refresh();
+        this._promiseUnblock = null;
+        throw ex;
+      }
+
+      this.succeeded = true;
+      this.hasBlockedData = false;
+      this._notifyChange();
+      yield this._succeed();
+    }.bind(this));
+
+    return this._promiseUnblock;
+  },
+
+  /**
+   * Confirms that a blocked download should be cleaned up.
+   *
+   * If a download was blocked but retained on disk this method can be used
+   * to remove the file.
+   *
+   * @return {Promise}
+   * @resolves When the Download's data has been removed.
+   * @rejects  JavaScript exception if any of the operations failed.
+   */
+  confirmBlock: function() {
+    if (this._promiseConfirmBlock) {
+      return this._promiseConfirmBlock;
+    }
+
+    if (this._promiseUnblock) {
+      return Promise.reject(new Error(
+        "Download is being unblocked, cannot confirmBlock."));
+    }
+
+    if (!this.hasBlockedData) {
+      return Promise.reject(new Error(
+        "confirmBlock may only be called on Downloads with blocked data."));
+    }
+
+    this._promiseConfirmBlock = Task.spawn(function* () {
+      try {
+        yield OS.File.remove(this.target.partFilePath);
+      } catch (ex) {
+        yield this.refresh();
+        this._promiseConfirmBlock = null;
+        throw ex;
+      }
+
+      this.hasBlockedData = false;
+      this._notifyChange();
+    }.bind(this));
+
+    return this._promiseConfirmBlock;
+  },
+
   /*
    * Launches the file after download has completed. This can open
    * the file with the default application for the target MIME type
    * or file extension, or with a custom application if launcherPath
    * is set.
    *
    * @return {Promise}
    * @resolves When the instruction to launch the file has been
@@ -767,31 +892,44 @@ this.Download.prototype = {
   refresh: function ()
   {
     return Task.spawn(function () {
       if (!this.stopped || this._finalized) {
         return;
       }
 
       // Update the current progress from disk if we retained partial data.
-      if (this.hasPartialData && this.target.partFilePath) {
-        let stat = yield OS.File.stat(this.target.partFilePath);
+      if ((this.hasPartialData || this.hasBlockedData) &&
+          this.target.partFilePath) {
+
+        try {
+          let stat = yield OS.File.stat(this.target.partFilePath);
+
+          // Ignore the result if the state has changed meanwhile.
+          if (!this.stopped || this._finalized) {
+            return;
+          }
 
-        // Ignore the result if the state has changed meanwhile.
-        if (!this.stopped || this._finalized) {
-          return;
+          // Update the bytes transferred and the related progress properties.
+          this.currentBytes = stat.size;
+          if (this.totalBytes > 0) {
+            this.hasProgress = true;
+            this.progress = Math.floor(this.currentBytes /
+                                           this.totalBytes * 100);
+          }
+        } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+          // Ignore the result if the state has changed meanwhile.
+          if (!this.stopped || this._finalized) {
+            return;
+          }
+
+          this.hasBlockedData = false;
+          this.hasPartialData = false;
         }
 
-        // Update the bytes transferred and the related progress properties.
-        this.currentBytes = stat.size;
-        if (this.totalBytes > 0) {
-          this.hasProgress = true;
-          this.progress = Math.floor(this.currentBytes /
-                                         this.totalBytes * 100);
-        }
         this._notifyChange();
       }
     }.bind(this)).then(null, Cu.reportError);
   },
 
   /**
    * True if the "finalize" method has been called.  This prevents the download
    * from starting again after having been stopped.
@@ -979,16 +1117,17 @@ this.Download.prototype = {
 /**
  * Defines which properties of the Download object are serializable.
  */
 const kPlainSerializableDownloadProperties = [
   "succeeded",
   "canceled",
   "totalBytes",
   "hasPartialData",
+  "hasBlockedData",
   "tryToKeepPartialData",
   "launcherPath",
   "launchWhenSucceeded",
   "contentType",
 ];
 
 /**
  * Creates a new Download object from a serializable representation.  This
@@ -1813,21 +1952,16 @@ this.DownloadCopySaver.prototype = {
               try {
                 backgroundFileSaver.onStopRequest(aRequest, aContext,
                                                   aStatusCode);
               } finally {
                 // If the data transfer completed successfully, indicate to the
                 // background file saver that the operation can finish.  If the
                 // data transfer failed, the saver has been already stopped.
                 if (Components.isSuccessCode(aStatusCode)) {
-                  if (partFilePath) {
-                    // Move to the final target if we were using a part file.
-                    backgroundFileSaver.setTarget(
-                                        new FileUtils.File(targetPath), false);
-                  }
                   backgroundFileSaver.finish(Cr.NS_OK);
                 }
               }
             }.bind(copySaver),
 
             onDataAvailable: function (aRequest, aContext, aInputStream,
                                        aOffset, aCount) {
               backgroundFileSaver.onDataAvailable(aRequest, aContext,
@@ -1850,16 +1984,18 @@ this.DownloadCopySaver.prototype = {
           // the download, ensure that we release the resources of the saver.
           backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
           throw ex;
         }
 
         // We will wait on this promise in case no error occurred while setting
         // up the chain of objects for the download.
         yield deferSaveComplete.promise;
+
+        yield this._checkReputationAndMove();
       } catch (ex) {
         // Ensure we always remove the placeholder for the final target file on
         // failure, independently of which code path failed.  In some cases, the
         // background file saver may have already removed the file.
         try {
           yield OS.File.remove(targetPath);
         } catch (e2) {
           // If we failed during the operation, we report the error but use the
@@ -1872,16 +2008,57 @@ this.DownloadCopySaver.prototype = {
           }
         }
         throw ex;
       }
     }.bind(this));
   },
 
   /**
+   * Perform the reputation check and cleanup the downloaded data if required.
+   * If the download passes the reputation check and is using a part file we
+   * will move it to the target path since reputation checking is the final
+   * step in the saver.
+   *
+   * @return {Promise}
+   * @resolves When the reputation check and cleanup is complete.
+   * @rejects DownloadError if the download should be blocked.
+   */
+  _checkReputationAndMove: Task.async(function* () {
+    let download = this.download;
+    let targetPath = this.download.target.path;
+    let partFilePath = this.download.target.partFilePath;
+
+    if (yield DownloadIntegration.shouldBlockForReputationCheck(download)) {
+      download.progress = 100;
+      download.hasPartialData = false;
+
+      // We will remove the potentially dangerous file if instructed by
+      // DownloadIntegration. We will always remove the file when the
+      // download did not use a partial file path, meaning it
+      // currently has its final filename.
+      if (!DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
+        try {
+          yield OS.File.remove(partFilePath || targetPath);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      } else {
+        download.hasBlockedData = true;
+      }
+
+      throw new DownloadError({ becauseBlockedByReputationCheck: true });
+    }
+
+    if (partFilePath) {
+      yield OS.File.move(partFilePath, targetPath);
+    }
+  }),
+
+  /**
    * Implements "DownloadSaver.cancel".
    */
   cancel: function DCS_cancel()
   {
     this._canceled = true;
     if (this._backgroundFileSaver) {
       this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
       this._backgroundFileSaver = null;
@@ -2166,30 +2343,32 @@ this.DownloadLegacySaver.prototype = {
           aSetProgressBytesFn(0, this.request.contentLength);
         }
 
         // If the component executing the download provides the path of a
         // ".part" file, it means that it expects the listener to move the file
         // to its final target path when the download succeeds.  In this case,
         // an empty ".part" file is created even if no data was received from
         // the source.
-        if (this.download.target.partFilePath) {
-          yield OS.File.move(this.download.target.partFilePath,
-                             this.download.target.path);
-        } else {
-          // The download implementation may not have created the target file if
-          // no data was received from the source.  In this case, ensure that an
-          // empty file is created as expected.
+        //
+        // When no ".part" file path is provided the download implementation may
+        // not have created the target file (if no data was received from the
+        // source).  In this case, ensure that an empty file is created as
+        // expected.
+        if (!this.download.target.partFilePath) {
           try {
             // This atomic operation is more efficient than an existence check.
             let file = yield OS.File.open(this.download.target.path,
                                           { create: true });
             yield file.close();
           } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { }
         }
+
+        yield this._checkReputationAndMove();
+
       } catch (ex) {
         // Ensure we always remove the final target file on failure,
         // independently of which code path failed.  In some cases, the
         // component executing the download may have already removed the file.
         try {
           yield OS.File.remove(this.download.target.path);
         } catch (e2) {
           // If we failed during the operation, we report the error but use the
@@ -2212,16 +2391,20 @@ this.DownloadLegacySaver.prototype = {
         this.request = null;
         this.deferCanceled = null;
         // Allow the download to restart through a DownloadCopySaver.
         this.firstExecutionFinished = true;
       }
     }.bind(this));
   },
 
+  _checkReputationAndMove: function () {
+    return DownloadCopySaver.prototype._checkReputationAndMove.call(this);
+  },
+
   /**
    * Implements "DownloadSaver.cancel".
    */
   cancel: function DLS_cancel()
   {
     // We may be using a DownloadCopySaver to handle resuming.
     if (this.copySaver) {
       return this.copySaver.cancel.apply(this.copySaver, arguments);
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -147,16 +147,17 @@ this.DownloadIntegration = {
   dontCheckParentalControls: false,
   shouldBlockInTest: false,
 #ifdef MOZ_URL_CLASSIFIER
   dontCheckApplicationReputation: false,
 #else
   dontCheckApplicationReputation: true,
 #endif
   shouldBlockInTestForApplicationReputation: false,
+  shouldKeepBlockedDataInTest: false,
   dontOpenFileAndFolder: false,
   downloadDoneCalled: false,
   _deferTestOpenFile: null,
   _deferTestShowDir: null,
   _deferTestClearPrivateList: null,
 
   /**
    * Main DownloadStore object for loading and saving the list of persistent
@@ -170,16 +171,40 @@ this.DownloadIntegration = {
    */
   get testMode() this._testMode,
   set testMode(mode) {
     this._downloadsDirectory = null;
     return (this._testMode = mode);
   },
 
   /**
+   * Returns whether data for blocked downloads should be kept on disk.
+   * Implementations which support unblocking downloads may return true to
+   * keep the blocked download on disk until its fate is decided.
+   *
+   * If a download is blocked and the partial data is kept the Download's
+   * 'hasBlockedData' property will be true. In this state Download.unblock()
+   * or Download.confirmBlock() may be used to either unblock the download or
+   * remove the downloaded data respectively.
+   *
+   * Even if shouldKeepBlockedData returns true, if the download did not use a
+   * partFile the blocked data will be removed - preventing the complete
+   * download from existing on disk with its final filename.
+   *
+   * @return boolean True if data should be kept.
+   */
+  shouldKeepBlockedData: function() {
+    if (this.shouldBlockInTestForApplicationReputation) {
+      return this.shouldKeepBlockedDataInTest;
+    }
+
+    return false;
+  },
+
+  /**
    * Performs initialization of the list of persistent downloads, before its
    * first use by the host application.  This function may be called only once
    * during the entire lifetime of the application.
    *
    * @param aList
    *        DownloadList object to be populated with the download objects
    *        serialized from the previous session.  This list will be persisted
    *        to disk during the session lifetime.
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1542,52 +1542,241 @@ add_task(function test_getSha256Hash()
   if (!gUseLegacySaver) {
     let download = yield promiseStartDownload(httpUrl("source.txt"));
     yield promiseDownloadStopped(download);
     do_check_true(download.stopped);
     do_check_eq(32, download.saver.getSha256Hash().length);
   }
 });
 
+
+/**
+ * Create a download which will be reputation blocked.
+ *
+ * @param options
+ *        {
+ *           keepPartialData: bool,
+ *           keepBlockedData: bool,
+ *        }
+ * @return {Promise}
+ * @resolves The reputation blocked download.
+ * @rejects JavaScript exception.
+ */
+let promiseBlockedDownload = Task.async(function* (options) {
+  function cleanup() {
+    DownloadIntegration.shouldBlockInTestForApplicationReputation = false;
+    DownloadIntegration.shouldKeepBlockedDataInTest = false;
+  }
+  do_register_cleanup(cleanup);
+
+  let {keepPartialData, keepBlockedData} = options;
+  DownloadIntegration.shouldBlockInTestForApplicationReputation = true;
+  DownloadIntegration.shouldKeepBlockedDataInTest = keepBlockedData;
+
+  let download;
+
+  try {
+    if (keepPartialData) {
+      download = yield promiseStartDownload_tryToKeepPartialData();
+      continueResponses();
+    } else if (gUseLegacySaver) {
+      download = yield promiseStartLegacyDownload();
+    } else {
+      download = yield promiseNewDownload();
+      yield download.start();
+      do_throw("The download should have blocked.");
+    }
+
+    yield promiseDownloadStopped(download);
+    do_throw("The download should have blocked.");
+  } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) {
+    do_check_true(ex.becauseBlockedByReputationCheck);
+    do_check_true(download.error.becauseBlockedByReputationCheck);
+  }
+
+  do_check_true(download.stopped);
+  do_check_false(download.succeeded);
+  do_check_false(yield OS.File.exists(download.target.path));
+
+  cleanup();
+  return download;
+});
+
 /**
  * Checks that application reputation blocks the download and the target file
  * does not exist.
  */
 add_task(function test_blocked_applicationReputation()
 {
-  function cleanup() {
-    DownloadIntegration.shouldBlockInTestForApplicationReputation = false;
-  }
-  do_register_cleanup(cleanup);
-  DownloadIntegration.shouldBlockInTestForApplicationReputation = true;
-
-  let download;
-  try {
-    if (!gUseLegacySaver) {
-      // When testing DownloadCopySaver, we want to check that the promise
-      // returned by the "start" method is rejected.
-      download = yield promiseNewDownload();
-      yield download.start();
-    } else {
-      // When testing DownloadLegacySaver, we cannot be sure whether we are
-      // testing the promise returned by the "start" method or we are testing
-      // the "error" property checked by promiseDownloadStopped.  This happens
-      // because we don't have control over when the download is started.
-      download = yield promiseStartLegacyDownload();
-      yield promiseDownloadStopped(download);
-    }
-    do_throw("The download should have blocked.");
-  } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) {
-    do_check_true(ex.becauseBlockedByReputationCheck);
-    do_check_true(download.error.becauseBlockedByReputationCheck);
-  }
+  let download = yield promiseBlockedDownload({
+    keepPartialData: false,
+    keepBlockedData: false,
+  });
 
   // Now that the download is blocked, the target file should not exist.
   do_check_false(yield OS.File.exists(download.target.path));
-  cleanup();
+
+  // There should also be no blocked data in this case
+  do_check_false(download.hasBlockedData);
+});
+
+/**
+ * Checks that application reputation blocks the download but maintains the
+ * blocked data, which will be deleted when the block is confirmed.
+ */
+add_task(function test_blocked_applicationReputation_confirmBlock()
+{
+  let download = yield promiseBlockedDownload({
+    keepPartialData: true,
+    keepBlockedData: true,
+  });
+
+  do_check_true(download.hasBlockedData);
+  do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+  yield download.confirmBlock();
+
+  // After confirming the block the download should be in a failed state and
+  // have no downloaded data left on disk.
+  do_check_true(download.stopped);
+  do_check_false(download.succeeded);
+  do_check_false(download.hasBlockedData);
+  do_check_false(yield OS.File.exists(download.target.partFilePath));
+  do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Checks that application reputation blocks the download but maintains the
+ * blocked data, which will be used to complete the download when unblocking.
+ */
+add_task(function test_blocked_applicationReputation_unblock()
+{
+  let download = yield promiseBlockedDownload({
+    keepPartialData: true,
+    keepBlockedData: true,
+  });
+
+  do_check_true(download.hasBlockedData);
+  do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+  yield download.unblock();
+
+  // After unblocking the download should have succeeded and be
+  // present at the final path.
+  do_check_true(download.stopped);
+  do_check_true(download.succeeded);
+  do_check_false(download.hasBlockedData);
+  do_check_false(yield OS.File.exists(download.target.partFilePath));
+  do_check_true(yield OS.File.exists(download.target.path));
+
+  // The only indication the download was previously blocked is the
+  // existence of the error, so we make sure it's still set.
+  do_check_true(download.error instanceof Downloads.Error);
+  do_check_true(download.error.becauseBlocked);
+  do_check_true(download.error.becauseBlockedByReputationCheck);
+});
+
+/**
+ * Check that calling cancel on a blocked download will not cause errors
+ */
+add_task(function test_blocked_applicationReputation_cancel()
+{
+  let download = yield promiseBlockedDownload({
+    keepPartialData: true,
+    keepBlockedData: true,
+  });
+
+  // This call should succeed on a blocked download.
+  yield download.cancel();
+
+  // Calling cancel should not have changed the current state, the download
+  // should still be blocked.
+  do_check_true(download.error.becauseBlockedByReputationCheck);
+  do_check_true(download.stopped);
+  do_check_false(download.succeeded);
+  do_check_true(download.hasBlockedData);
+});
+
+/**
+ * Checks that unblock and confirmBlock cannot race on a blocked download
+ */
+add_task(function test_blocked_applicationReputation_decisionRace()
+{
+  let download = yield promiseBlockedDownload({
+    keepPartialData: true,
+    keepBlockedData: true,
+  });
+
+  let unblockPromise = download.unblock();
+  let confirmBlockPromise = download.confirmBlock();
+
+  yield confirmBlockPromise.then(() => {
+    do_throw("confirmBlock should have failed.");
+  }, () => {});
+
+  yield unblockPromise;
+
+  // After unblocking the download should have succeeded and be
+  // present at the final path.
+  do_check_true(download.stopped);
+  do_check_true(download.succeeded);
+  do_check_false(download.hasBlockedData);
+  do_check_false(yield OS.File.exists(download.target.partFilePath));
+  do_check_true(yield OS.File.exists(download.target.path));
+
+  download = yield promiseBlockedDownload({
+    keepPartialData: true,
+    keepBlockedData: true,
+  });
+
+  confirmBlockPromise = download.confirmBlock();
+  unblockPromise = download.unblock();
+
+  yield unblockPromise.then(() => {
+    do_throw("unblock should have failed.");
+  }, () => {});
+
+  yield confirmBlockPromise;
+
+  // After confirming the block the download should be in a failed state and
+  // have no downloaded data left on disk.
+  do_check_true(download.stopped);
+  do_check_false(download.succeeded);
+  do_check_false(download.hasBlockedData);
+  do_check_false(yield OS.File.exists(download.target.partFilePath));
+  do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Checks that unblocking a blocked download fails if the blocked data has been
+ * removed.
+ */
+add_task(function test_blocked_applicationReputation_unblock()
+{
+  let download = yield promiseBlockedDownload({
+    keepPartialData: true,
+    keepBlockedData: true,
+  });
+
+  do_check_true(download.hasBlockedData);
+  do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+  // Remove the blocked data without telling the download.
+  yield OS.File.remove(download.target.partFilePath);
+
+  let unblockPromise = download.unblock();
+  yield unblockPromise.then(() => {
+    do_throw("unblock should have failed.");
+  }, () => {});
+
+  // Even though unblocking failed the download state should have been updated
+  // to reflect the lack of blocked data.
+  do_check_false(download.hasBlockedData);
+  do_check_true(download.stopped);
+  do_check_false(download.succeeded);
 });
 
 /**
  * download.showContainingDirectory() action
  */
 add_task(function test_showContainingDirectory() {
   DownloadIntegration._deferTestShowDir = Promise.defer();