Bug 1022594 Part 1 Change Loop's incoming call handling to get the call details before displaying the incoming call UI. r=nperriault
authorMark Banner <standard8@mozilla.com>
Tue, 05 Aug 2014 15:54:55 +0100
changeset 197895 9632c9041c1d5a9f5db0c94271b914c46b6507f3
parent 197894 9a3b88192df405946deab31d1d0fa68304e654f3
child 197896 85b7352ac461513bc8f41d85261f823681ff70e3
push id27254
push userryanvm@gmail.com
push dateTue, 05 Aug 2014 19:43:11 +0000
treeherdermozilla-central@191e834ff32b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault
bugs1022594
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1022594 Part 1 Change Loop's incoming call handling to get the call details before displaying the incoming call UI. r=nperriault
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/shared/js/models.js
browser/components/loop/content/shared/js/router.js
browser/components/loop/standalone/content/js/webapp.js
browser/components/loop/standalone/content/js/webapp.jsx
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/shared/models_test.js
browser/components/loop/test/shared/router_test.js
browser/components/loop/test/standalone/webapp_test.js
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -153,39 +153,52 @@ loop.conversation = (function(OT, mozL10
      * Incoming call route.
      *
      * @param {String} loopVersion The version from the push notification, set
      *                             by the router from the URL.
      */
     incoming: function(loopVersion) {
       navigator.mozLoop.startAlerting();
       this._conversation.set({loopVersion: loopVersion});
-      this._conversation.once("accept", function() {
+      this._conversation.once("accept", () => {
         this.navigate("call/accept", {trigger: true});
-      }.bind(this));
-      this._conversation.once("decline", function() {
+      });
+      this._conversation.once("decline", () => {
         this.navigate("call/decline", {trigger: true});
-      }.bind(this));
-      this._conversation.once("declineAndBlock", function() {
+      });
+      this._conversation.once("declineAndBlock", () => {
         this.navigate("call/declineAndBlock", {trigger: true});
-      }.bind(this));
-      this.loadReactComponent(loop.conversation.IncomingCallView({
-        model: this._conversation
-      }));
+      });
+      this._conversation.once("call:incoming", this.startCall, this);
+      this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
+        if (err) {
+          console.error("Failed to get the sessionData", err);
+          // XXX Not the ideal response, but bug 1047410 will be replacing
+          //this by better "call failed" UI.
+          this._notifier.errorL10n("cannot_start_call_session_not_ready");
+          return;
+        }
+        // XXX For incoming calls we might have more than one call queued.
+        // For now, we'll just assume the first call is the right information.
+        // We'll probably really want to be getting this data from the
+        // background worker on the desktop client.
+        // Bug 1032700 should fix this.
+        this._conversation.setSessionData(sessionData[0]);
+        this.loadReactComponent(loop.conversation.IncomingCallView({
+          model: this._conversation
+        }));
+      });
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
-      this._conversation.initiate({
-        client: new loop.Client(),
-        outgoing: false
-      });
+      this._conversation.incoming();
     },
 
     /**
      * Declines an incoming call.
      */
     decline: function() {
       navigator.mozLoop.stopAlerting();
       // XXX For now, we just close the window
@@ -196,20 +209,20 @@ loop.conversation = (function(OT, mozL10
      * Decline and block an incoming call
      * @note:
      * - loopToken is the callUrl identifier. It gets set in the panel
      *   after a callUrl is received
      */
     declineAndBlock: function() {
       navigator.mozLoop.stopAlerting();
       var token = navigator.mozLoop.getLoopCharPref('loopToken');
-      var client = new loop.Client();
-      client.deleteCallUrl(token, function(error) {
+      this._client.deleteCallUrl(token, function(error) {
         // XXX The conversation window will be closed when this cb is triggered
         // figure out if there is a better way to report the error to the user
+        // (bug 1048909).
         console.log(error);
       });
       window.close();
     },
 
     /**
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
@@ -249,18 +262,22 @@ loop.conversation = (function(OT, mozL10
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     document.title = mozL10n.get("incoming_call_title");
 
+    var client = new loop.Client();
     router = new ConversationRouter({
-      conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
+      client: client,
+      conversation: new loop.shared.models.ConversationModel(
+        {},         // Model attributes
+        {sdk: OT}), // Model dependencies
       notifier: new sharedViews.NotificationListView({el: "#messages"})
     });
     Backbone.history.start();
   }
 
   return {
     ConversationRouter: ConversationRouter,
     IncomingCallView: IncomingCallView,
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -153,39 +153,52 @@ loop.conversation = (function(OT, mozL10
      * Incoming call route.
      *
      * @param {String} loopVersion The version from the push notification, set
      *                             by the router from the URL.
      */
     incoming: function(loopVersion) {
       navigator.mozLoop.startAlerting();
       this._conversation.set({loopVersion: loopVersion});
-      this._conversation.once("accept", function() {
+      this._conversation.once("accept", () => {
         this.navigate("call/accept", {trigger: true});
-      }.bind(this));
-      this._conversation.once("decline", function() {
+      });
+      this._conversation.once("decline", () => {
         this.navigate("call/decline", {trigger: true});
-      }.bind(this));
-      this._conversation.once("declineAndBlock", function() {
+      });
+      this._conversation.once("declineAndBlock", () => {
         this.navigate("call/declineAndBlock", {trigger: true});
-      }.bind(this));
-      this.loadReactComponent(loop.conversation.IncomingCallView({
-        model: this._conversation
-      }));
+      });
+      this._conversation.once("call:incoming", this.startCall, this);
+      this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
+        if (err) {
+          console.error("Failed to get the sessionData", err);
+          // XXX Not the ideal response, but bug 1047410 will be replacing
+          //this by better "call failed" UI.
+          this._notifier.errorL10n("cannot_start_call_session_not_ready");
+          return;
+        }
+        // XXX For incoming calls we might have more than one call queued.
+        // For now, we'll just assume the first call is the right information.
+        // We'll probably really want to be getting this data from the
+        // background worker on the desktop client.
+        // Bug 1032700 should fix this.
+        this._conversation.setSessionData(sessionData[0]);
+        this.loadReactComponent(loop.conversation.IncomingCallView({
+          model: this._conversation
+        }));
+      });
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
-      this._conversation.initiate({
-        client: new loop.Client(),
-        outgoing: false
-      });
+      this._conversation.incoming();
     },
 
     /**
      * Declines an incoming call.
      */
     decline: function() {
       navigator.mozLoop.stopAlerting();
       // XXX For now, we just close the window
@@ -196,20 +209,20 @@ loop.conversation = (function(OT, mozL10
      * Decline and block an incoming call
      * @note:
      * - loopToken is the callUrl identifier. It gets set in the panel
      *   after a callUrl is received
      */
     declineAndBlock: function() {
       navigator.mozLoop.stopAlerting();
       var token = navigator.mozLoop.getLoopCharPref('loopToken');
-      var client = new loop.Client();
-      client.deleteCallUrl(token, function(error) {
+      this._client.deleteCallUrl(token, function(error) {
         // XXX The conversation window will be closed when this cb is triggered
         // figure out if there is a better way to report the error to the user
+        // (bug 1048909).
         console.log(error);
       });
       window.close();
     },
 
     /**
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
@@ -249,18 +262,22 @@ loop.conversation = (function(OT, mozL10
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     document.title = mozL10n.get("incoming_call_title");
 
+    var client = new loop.Client();
     router = new ConversationRouter({
-      conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
+      client: client,
+      conversation: new loop.shared.models.ConversationModel(
+        {},         // Model attributes
+        {sdk: OT}), // Model dependencies
       notifier: new sharedViews.NotificationListView({el: "#messages"})
     });
     Backbone.history.start();
   }
 
   return {
     ConversationRouter: ConversationRouter,
     IncomingCallView: IncomingCallView,
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -74,106 +74,76 @@ loop.shared.models = (function() {
       this.sdk = options.sdk;
       this.pendingCallTimeout = options.pendingCallTimeout || 20000;
 
       // Ensure that any pending call timer is cleared on disconnect/error
       this.on("session:ended session:error", this._clearPendingCallTimer, this);
     },
 
     /**
-     * Initiates a conversation, requesting call session information to the Loop
-     * server and updates appropriately the current model attributes with the
-     * data.
-     *
-     * Available options:
+     * Starts an incoming conversation.
+     */
+    incoming: function() {
+      this.trigger("call:incoming");
+    },
+
+    /**
+     * Used to indicate that an outgoing call should start any necessary
+     * set-up.
+     */
+    setupOutgoingCall: function() {
+      this.trigger("call:outgoing:setup");
+    },
+
+    /**
+     * Starts an outgoing conversation.
      *
-     * - {Boolean} outgoing Set to true if this model represents the
-     *                            outgoing call.
-     * - {Boolean} callType Only valid for outgoing calls. The type of media in
-     *                      the call, e.g. "audio" or "audio-video"
-     * - {loop.shared.Client} client  A client object to request call information
-     *                                from. Expects requestCallInfo for outgoing
-     *                                calls, requestCallsInfo for incoming calls.
-     *
-     * Triggered events:
-     *
-     * - `session:ready` when the session information have been successfully
-     *   retrieved from the server;
-     * - `session:error` when the request failed.
-     *
-     * @param {Object} options Options object
+     * @param {Object} sessionData The session data received from the
+     *                             server for the outgoing call.
      */
-    initiate: function(options) {
-      options = options || {};
+    outgoing: function(sessionData) {
+      this._clearPendingCallTimer();
 
       // Outgoing call has never reached destination, closing - see bug 1020448
       function handleOutgoingCallTimeout() {
         /*jshint validthis:true */
         if (!this.get("ongoing")) {
           this.trigger("timeout").endSession();
         }
       }
 
-      function handleResult(err, sessionData) {
-        /*jshint validthis:true */
-        this._clearPendingCallTimer();
-
-        if (err) {
-          this._handleServerError(err);
-          return;
-        }
+      // Setup pending call timeout.
+      this._pendingCallTimer = setTimeout(
+        handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
 
-        if (options.outgoing) {
-          // Setup pending call timeout.
-          this._pendingCallTimer = setTimeout(
-            handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
-        } else {
-          // XXX For incoming calls we might have more than one call queued.
-          // For now, we'll just assume the first call is the right information.
-          // We'll probably really want to be getting this data from the
-          // background worker on the desktop client.
-          // Bug 990714 should fix this.
-          sessionData = sessionData[0];
-        }
-
-        this.setReady(sessionData);
-      }
-
-      if (options.outgoing) {
-        options.client.requestCallInfo(this.get("loopToken"), options.callType,
-          handleResult.bind(this));
-      }
-      else {
-        options.client.requestCallsInfo(this.get("loopVersion"),
-          handleResult.bind(this));
-      }
+      this.setSessionData(sessionData);
+      this.trigger("call:outgoing");
     },
 
     /**
      * Checks that the session is ready.
      *
      * @return {Boolean}
      */
     isSessionReady: function() {
       return !!this.get("sessionId");
     },
 
     /**
-     * Sets session information and triggers the `session:ready` event.
+     * Sets session information.
      *
      * @param {Object} sessionData Conversation session information.
      */
-    setReady: function(sessionData) {
+    setSessionData: function(sessionData) {
       // Explicit property assignment to prevent later "surprises"
       this.set({
         sessionId:    sessionData.sessionId,
         sessionToken: sessionData.sessionToken,
         apiKey:       sessionData.apiKey
-      }).trigger("session:ready", this);
-      return this;
+      });
     },
 
     /**
      * Starts a SDK session and subscribe to call events.
      */
     startSession: function() {
       if (!this.isSessionReady()) {
         throw new Error("Can't start session as it's not ready");
--- a/browser/components/loop/content/shared/js/router.js
+++ b/browser/components/loop/content/shared/js/router.js
@@ -116,19 +116,22 @@ loop.shared.router = (function(l10n) {
      *
      * @param {Object} options Options object.
      */
     constructor: function(options) {
       options = options || {};
       if (!options.conversation) {
         throw new Error("missing required conversation");
       }
+      if (!options.client) {
+        throw new Error("missing required client");
+      }
       this._conversation = options.conversation;
+      this._client = options.client;
 
-      this.listenTo(this._conversation, "session:ready", this._onSessionReady);
       this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
       this.listenTo(this._conversation, "session:peer-hungup",
                                         this._onPeerHungup);
       this.listenTo(this._conversation, "session:network-disconnected",
                                         this._onNetworkDisconnected);
       this.listenTo(this._conversation, "session:connection-error",
                     this._notifyError);
 
@@ -141,33 +144,21 @@ loop.shared.router = (function(l10n) {
      */
     _notifyError: function(error) {
       console.log(error);
       this._notifier.errorL10n("connection_error_see_console_notification");
       this.endCall();
     },
 
     /**
-     * Starts the call. This method should be overriden.
-     */
-    startCall: function() {},
-
-    /**
      * Ends the call. This method should be overriden.
      */
     endCall: function() {},
 
     /**
-     * Session is ready.
-     */
-    _onSessionReady: function() {
-      this.startCall();
-    },
-
-    /**
      * Session has ended. Notifies the user and ends the call.
      */
     _onSessionEnded: function() {
       this.endCall();
     },
 
     /**
      * Peer hung up. Notifies the user and ends the call.
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -126,17 +126,17 @@ loop.webapp = (function($, _, OT, webL10
       );
     }
   });
 
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
    */
-  var ConversationFormView = React.createClass({displayName: 'ConversationFormView',
+  var StartConversationView = React.createClass({displayName: 'StartConversationView',
     /**
      * Constructor.
      *
      * Required options:
      * - {loop.shared.model.ConversationModel}    model    Conversation model.
      * - {loop.shared.views.NotificationListView} notifier Notifier component.
      *
      */
@@ -169,29 +169,19 @@ loop.webapp = (function($, _, OT, webL10
     _onSessionError: function(error) {
       console.error(error);
       this.props.notifier.errorL10n("unable_retrieve_call_info");
     },
 
     /**
      * Initiates the call.
      */
-    _initiate: function() {
-      this.props.model.initiate({
-        client: new loop.StandaloneClient({
-          baseServerUrl: baseServerUrl
-        }),
-        outgoing: true,
-        // For now, we assume both audio and video as there is no
-        // other option to select.
-        callType: "audio-video",
-        loopServer: loop.config.serverUrl
-      });
-
+    _initiateOutgoingCall: function() {
       this.setState({disableCallButton: true});
+      this.props.model.setupOutgoingCall();
     },
 
     _setConversationTimestamp: function(err, callUrlInfo) {
       if (err) {
         this.props.notifier.errorL10n("unable_retrieve_call_info");
       } else {
         var date = (new Date(callUrlInfo.urlCreationDate * 1000));
         var options = {year: "numeric", month: "long", day: "numeric"};
@@ -226,17 +216,17 @@ loop.webapp = (function($, _, OT, webL10
             React.DOM.p({className: "large-font light-weight-font"}, 
               __("initiate_call_button_label")
             ), 
 
             React.DOM.div({id: "messages"}), 
 
             React.DOM.div({className: "button-group"}, 
               React.DOM.div({className: "flex-padding-1"}), 
-              React.DOM.button({ref: "submitButton", onClick: this._initiate, 
+              React.DOM.button({ref: "submitButton", onClick: this._initiateOutgoingCall, 
                 className: callButtonClasses, 
                 disabled: this.state.disableCallButton}, 
                 __("initiate_call_button"), 
                 React.DOM.i({className: "icon icon-video"})
               ), 
               React.DOM.div({className: "flex-padding-1"})
             ), 
 
@@ -262,40 +252,75 @@ loop.webapp = (function($, _, OT, webL10
       "call/expired":        "expired",
       "call/ongoing/:token": "loadConversation",
       "call/:token":         "initiate"
     },
 
     initialize: function(options) {
       this.helper = options.helper;
       if (!this.helper) {
-        throw new Error("WebappRouter requires an helper object");
+        throw new Error("WebappRouter requires a helper object");
       }
 
       // Load default view
       this.loadView(new HomeView());
 
       this.listenTo(this._conversation, "timeout", this._onTimeout);
-      this.listenTo(this._conversation, "session:expired",
-                    this._onSessionExpired);
     },
 
     _onSessionExpired: function() {
       this.navigate("/call/expired", {trigger: true});
     },
 
     /**
-     * @override {loop.shared.router.BaseConversationRouter.startCall}
+     * Starts the set up of a call, obtaining the required information from the
+     * server.
      */
-    startCall: function() {
-      if (!this._conversation.get("loopToken")) {
+    setupOutgoingCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
         this._notifier.errorL10n("missing_conversation_info");
         this.navigate("home", {trigger: true});
       } else {
-        this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
+        this._conversation.once("call:outgoing", this.startCall, this);
+
+        // XXX For now, we assume both audio and video as there is no
+        // other option to select (bug 1048333)
+        this._client.requestCallInfo(this._conversation.get("loopToken"), "audio-video",
+                                     (err, sessionData) => {
+          if (err) {
+            switch (err.errno) {
+              // loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
+              // missing OR expired; we treat this information as if the url is always
+              // expired.
+              case 105:
+                this._onSessionExpired();
+                break;
+              default:
+                this._notifier.errorL10n("missing_conversation_info");
+                this.navigate("home", {trigger: true});
+                break;
+            }
+            return;
+          }
+          this._conversation.outgoing(sessionData);
+        });
+      }
+    },
+
+    /**
+     * Actually starts the call.
+     */
+    startCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
+        this._notifier.errorL10n("missing_conversation_info");
+        this.navigate("home", {trigger: true});
+      } else {
+        this.navigate("call/ongoing/" + loopToken, {
           trigger: true
         });
       }
     },
 
     /**
      * @override {loop.shared.router.BaseConversationRouter.endCall}
      */
@@ -338,23 +363,24 @@ loop.webapp = (function($, _, OT, webL10
      * @param  {String} loopToken Loop conversation token.
      */
     initiate: function(loopToken) {
       // Check if a session is ongoing; if so, terminate it
       if (this._conversation.get("ongoing")) {
         this._conversation.endSession();
       }
       this._conversation.set("loopToken", loopToken);
-      this.loadReactComponent(ConversationFormView({
+
+      var startView = StartConversationView({
         model: this._conversation,
         notifier: this._notifier,
-        client: new loop.StandaloneClient({
-          baseServerUrl: loop.config.serverUrl
-        })
-      }));
+        client: this._client
+      });
+      this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
+      this.loadReactComponent(startView);
     },
 
     /**
      * Loads conversation establishment view.
      *
      */
     loadConversation: function(loopToken) {
       if (!this._conversation.isSessionReady()) {
@@ -385,19 +411,23 @@ loop.webapp = (function($, _, OT, webL10
     }
   };
 
   /**
    * App initialization.
    */
   function init() {
     var helper = new WebappHelper();
+    var client = new loop.StandaloneClient({
+      baseServerUrl: baseServerUrl
+    }),
     router = new WebappRouter({
       helper: helper,
       notifier: new sharedViews.NotificationListView({el: "#messages"}),
+      client: client,
       conversation: new sharedModels.ConversationModel({}, {
         sdk: OT,
         pendingCallTimeout: loop.config.pendingCallTimeout
       })
     });
     Backbone.history.start();
     if (helper.isIOS(navigator.platform)) {
       router.navigate("unsupportedDevice", {trigger: true});
@@ -407,16 +437,16 @@ loop.webapp = (function($, _, OT, webL10
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = document.webL10n.getLanguage();
     document.documentElement.dir = document.webL10n.getDirection();
   }
 
   return {
     baseServerUrl: baseServerUrl,
     CallUrlExpiredView: CallUrlExpiredView,
-    ConversationFormView: ConversationFormView,
+    StartConversationView: StartConversationView,
     HomeView: HomeView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRouter: WebappRouter
   };
 })(jQuery, _, window.OT, document.webL10n);
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -126,17 +126,17 @@ loop.webapp = (function($, _, OT, webL10
       );
     }
   });
 
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
    */
-  var ConversationFormView = React.createClass({
+  var StartConversationView = React.createClass({
     /**
      * Constructor.
      *
      * Required options:
      * - {loop.shared.model.ConversationModel}    model    Conversation model.
      * - {loop.shared.views.NotificationListView} notifier Notifier component.
      *
      */
@@ -169,29 +169,19 @@ loop.webapp = (function($, _, OT, webL10
     _onSessionError: function(error) {
       console.error(error);
       this.props.notifier.errorL10n("unable_retrieve_call_info");
     },
 
     /**
      * Initiates the call.
      */
-    _initiate: function() {
-      this.props.model.initiate({
-        client: new loop.StandaloneClient({
-          baseServerUrl: baseServerUrl
-        }),
-        outgoing: true,
-        // For now, we assume both audio and video as there is no
-        // other option to select.
-        callType: "audio-video",
-        loopServer: loop.config.serverUrl
-      });
-
+    _initiateOutgoingCall: function() {
       this.setState({disableCallButton: true});
+      this.props.model.setupOutgoingCall();
     },
 
     _setConversationTimestamp: function(err, callUrlInfo) {
       if (err) {
         this.props.notifier.errorL10n("unable_retrieve_call_info");
       } else {
         var date = (new Date(callUrlInfo.urlCreationDate * 1000));
         var options = {year: "numeric", month: "long", day: "numeric"};
@@ -226,17 +216,17 @@ loop.webapp = (function($, _, OT, webL10
             <p className="large-font light-weight-font">
               {__("initiate_call_button_label")}
             </p>
 
             <div id="messages"></div>
 
             <div className="button-group">
               <div className="flex-padding-1"></div>
-              <button ref="submitButton" onClick={this._initiate}
+              <button ref="submitButton" onClick={this._initiateOutgoingCall}
                 className={callButtonClasses}
                 disabled={this.state.disableCallButton}>
                 {__("initiate_call_button")}
                 <i className="icon icon-video"></i>
               </button>
               <div className="flex-padding-1"></div>
             </div>
 
@@ -262,40 +252,75 @@ loop.webapp = (function($, _, OT, webL10
       "call/expired":        "expired",
       "call/ongoing/:token": "loadConversation",
       "call/:token":         "initiate"
     },
 
     initialize: function(options) {
       this.helper = options.helper;
       if (!this.helper) {
-        throw new Error("WebappRouter requires an helper object");
+        throw new Error("WebappRouter requires a helper object");
       }
 
       // Load default view
       this.loadView(new HomeView());
 
       this.listenTo(this._conversation, "timeout", this._onTimeout);
-      this.listenTo(this._conversation, "session:expired",
-                    this._onSessionExpired);
     },
 
     _onSessionExpired: function() {
       this.navigate("/call/expired", {trigger: true});
     },
 
     /**
-     * @override {loop.shared.router.BaseConversationRouter.startCall}
+     * Starts the set up of a call, obtaining the required information from the
+     * server.
      */
-    startCall: function() {
-      if (!this._conversation.get("loopToken")) {
+    setupOutgoingCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
         this._notifier.errorL10n("missing_conversation_info");
         this.navigate("home", {trigger: true});
       } else {
-        this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
+        this._conversation.once("call:outgoing", this.startCall, this);
+
+        // XXX For now, we assume both audio and video as there is no
+        // other option to select (bug 1048333)
+        this._client.requestCallInfo(this._conversation.get("loopToken"), "audio-video",
+                                     (err, sessionData) => {
+          if (err) {
+            switch (err.errno) {
+              // loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
+              // missing OR expired; we treat this information as if the url is always
+              // expired.
+              case 105:
+                this._onSessionExpired();
+                break;
+              default:
+                this._notifier.errorL10n("missing_conversation_info");
+                this.navigate("home", {trigger: true});
+                break;
+            }
+            return;
+          }
+          this._conversation.outgoing(sessionData);
+        });
+      }
+    },
+
+    /**
+     * Actually starts the call.
+     */
+    startCall: function() {
+      var loopToken = this._conversation.get("loopToken");
+      if (!loopToken) {
+        this._notifier.errorL10n("missing_conversation_info");
+        this.navigate("home", {trigger: true});
+      } else {
+        this.navigate("call/ongoing/" + loopToken, {
           trigger: true
         });
       }
     },
 
     /**
      * @override {loop.shared.router.BaseConversationRouter.endCall}
      */
@@ -338,23 +363,24 @@ loop.webapp = (function($, _, OT, webL10
      * @param  {String} loopToken Loop conversation token.
      */
     initiate: function(loopToken) {
       // Check if a session is ongoing; if so, terminate it
       if (this._conversation.get("ongoing")) {
         this._conversation.endSession();
       }
       this._conversation.set("loopToken", loopToken);
-      this.loadReactComponent(ConversationFormView({
+
+      var startView = StartConversationView({
         model: this._conversation,
         notifier: this._notifier,
-        client: new loop.StandaloneClient({
-          baseServerUrl: loop.config.serverUrl
-        })
-      }));
+        client: this._client
+      });
+      this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
+      this.loadReactComponent(startView);
     },
 
     /**
      * Loads conversation establishment view.
      *
      */
     loadConversation: function(loopToken) {
       if (!this._conversation.isSessionReady()) {
@@ -385,19 +411,23 @@ loop.webapp = (function($, _, OT, webL10
     }
   };
 
   /**
    * App initialization.
    */
   function init() {
     var helper = new WebappHelper();
+    var client = new loop.StandaloneClient({
+      baseServerUrl: baseServerUrl
+    }),
     router = new WebappRouter({
       helper: helper,
       notifier: new sharedViews.NotificationListView({el: "#messages"}),
+      client: client,
       conversation: new sharedModels.ConversationModel({}, {
         sdk: OT,
         pendingCallTimeout: loop.config.pendingCallTimeout
       })
     });
     Backbone.history.start();
     if (helper.isIOS(navigator.platform)) {
       router.navigate("unsupportedDevice", {trigger: true});
@@ -407,16 +437,16 @@ loop.webapp = (function($, _, OT, webL10
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     document.documentElement.lang = document.webL10n.getLanguage();
     document.documentElement.dir = document.webL10n.getDirection();
   }
 
   return {
     baseServerUrl: baseServerUrl,
     CallUrlExpiredView: CallUrlExpiredView,
-    ConversationFormView: ConversationFormView,
+    StartConversationView: StartConversationView,
     HomeView: HomeView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRouter: WebappRouter
   };
 })(jQuery, _, window.OT, document.webL10n);
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -98,35 +98,39 @@ describe("loop.conversation", function()
     it("should start Backbone history", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(Backbone.history.start);
     });
   });
 
   describe("ConversationRouter", function() {
-    var conversation;
+    var conversation, client;
 
     beforeEach(function() {
+      client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {},
-        pendingCallTimeout: 1000
+        pendingCallTimeout: 1000,
       });
-      sandbox.stub(conversation, "initiate");
+      sandbox.stub(client, "requestCallsInfo");
+      sandbox.stub(conversation, "setSessionData");
     });
 
     describe("Routes", function() {
       var router;
 
       beforeEach(function() {
         router = new ConversationRouter({
+          client: client,
           conversation: conversation,
           notifier: notifier
         });
         sandbox.stub(router, "loadView");
+        sandbox.stub(conversation, "incoming");
       });
 
       describe("#incoming", function() {
 
         // XXX refactor to Just Work with "sandbox.stubComponent" or else
         // just pass in the sandbox and put somewhere generally usable
 
         function stubComponent(obj, component, mockTagName){
@@ -139,56 +143,88 @@ describe("loop.conversation", function()
           return sandbox.stub(obj, component, reactClass);
         }
 
         beforeEach(function() {
           sandbox.stub(router, "loadReactComponent");
           stubComponent(loop.conversation, "IncomingCallView");
         });
 
+        it("should start alerting", function() {
+          sandbox.stub(navigator.mozLoop, "startAlerting");
+          router.incoming("fakeVersion");
+
+          sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
+        });
+
         it("should set the loopVersion on the conversation model", function() {
           router.incoming("fakeVersion");
 
           expect(conversation.get("loopVersion")).to.equal("fakeVersion");
         });
 
-        it("should display the incoming call view", function() {
+        it("should call requestCallsInfo on the client",
+          function() {
+            router.incoming(42);
+
+            sinon.assert.calledOnce(client.requestCallsInfo);
+            sinon.assert.calledWith(client.requestCallsInfo, 42);
+          });
+
+        it("should display an error if requestCallsInfo returns an error",
+          function(){
+            client.requestCallsInfo.callsArgWith(1, "failed");
+
+            router.incoming(42);
+
+            sinon.assert.calledOnce(notifier.errorL10n);
+          });
+
+        describe("requestCallsInfo successful", function() {
+          var fakeSessionData;
+
+          beforeEach(function() {
+            fakeSessionData  = {
+              sessionId:    "sessionId",
+              sessionToken: "sessionToken",
+              apiKey:       "apiKey"
+            };
+
+            client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
+          });
+
+          it("should store the session data", function() {
+            router.incoming(42);
+
+            sinon.assert.calledOnce(conversation.setSessionData);
+            sinon.assert.calledWithExactly(conversation.setSessionData,
+                                           fakeSessionData);
+          });
+
+          it("should display the incoming call view", function() {
             router.incoming("fakeVersion");
 
             sinon.assert.calledOnce(loop.conversation.IncomingCallView);
             sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
                                            {model: conversation});
             sinon.assert.calledOnce(router.loadReactComponent);
             sinon.assert.calledWith(router.loadReactComponent,
               sinon.match(function(value) {
                 return TestUtils.isDescriptorOfType(value,
                   loop.conversation.IncomingCallView);
               }));
-        });
-
-        it("should start alerting", function() {
-          sandbox.stub(navigator.mozLoop, "startAlerting");
-          router.incoming("fakeVersion");
-
-          sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
+          });
         });
       });
 
       describe("#accept", function() {
         it("should initiate the conversation", function() {
           router.accept();
 
-          sinon.assert.calledOnce(conversation.initiate);
-          sinon.assert.calledWithMatch(conversation.initiate, {
-            client: {
-              mozLoop: navigator.mozLoop,
-              settings: {}
-            },
-            outgoing: false
-          });
+          sinon.assert.calledOnce(conversation.incoming);
         });
 
         it("should stop alerting", function() {
           sandbox.stub(navigator.mozLoop, "stopAlerting");
           router.accept();
 
           sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
         });
@@ -332,24 +368,27 @@ describe("loop.conversation", function()
           sessionId:    "sessionId",
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
         sandbox.stub(loop.conversation.ConversationRouter.prototype,
                      "navigate");
         conversation.set("loopToken", "fakeToken");
         router = new loop.conversation.ConversationRouter({
+          client: client,
           conversation: conversation,
           notifier: notifier
         });
       });
 
-      it("should navigate to call/ongoing once the call session is ready",
+      it("should navigate to call/ongoing once the call is ready",
         function() {
-          conversation.setReady(fakeSessionData);
+          router.incoming(42);
+
+          conversation.incoming();
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/ongoing");
         });
 
       it("should navigate to call/feedback when the call session ends",
         function() {
           conversation.trigger("session:ended");
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -73,161 +73,84 @@ describe("loop.shared.models", function(
         fakeClient = {
           requestCallInfo: sandbox.stub(),
           requestCallsInfo: sandbox.stub()
         };
         requestCallInfoStub = fakeClient.requestCallInfo;
         requestCallsInfoStub = fakeClient.requestCallsInfo;
       });
 
-      describe("#initiate", function() {
-        beforeEach(function() {
-          sandbox.stub(conversation, "endSession");
-        });
-
-        it("call requestCallInfo on the client for outgoing calls",
-          function() {
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true,
-              callType: "audio"
-            });
-
-            sinon.assert.calledOnce(requestCallInfoStub);
-            sinon.assert.calledWith(requestCallInfoStub, "fakeToken", "audio");
+      describe("#incoming", function() {
+        it("should trigger a `call:incoming` event", function(done) {
+          conversation.once("call:incoming", function() {
+            done();
           });
 
-        it("should not call requestCallsInfo on the client for outgoing calls",
-          function() {
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true,
-              callType: "audio"
-            });
-
-            sinon.assert.notCalled(requestCallsInfoStub);
-          });
+          conversation.incoming();
+        });
+      });
 
-        it("call requestCallsInfo on the client for incoming calls",
-          function() {
-            conversation.set("loopVersion", 42);
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: false
-            });
-
-            sinon.assert.calledOnce(requestCallsInfoStub);
-            sinon.assert.calledWith(requestCallsInfoStub, 42);
-          });
-
-        it("should not call requestCallInfo on the client for incoming calls",
-          function() {
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: false
-            });
-
-            sinon.assert.notCalled(requestCallInfoStub);
+      describe("#setupOutgoingCall", function() {
+        it("should trigger a `call:outgoing:setup` event", function(done) {
+          conversation.once("call:outgoing:setup", function() {
+            done();
           });
 
-        it("should update conversation session information from server data",
-          function() {
-            sandbox.stub(conversation, "setReady");
-            requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
+          conversation.setupOutgoingCall();
+        });
+      });
 
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true
-            });
+      describe("#outgoing", function() {
+        beforeEach(function() {
+          sandbox.stub(conversation, "endSession");
+          sandbox.stub(conversation, "setSessionData");
+        });
 
-            sinon.assert.calledOnce(conversation.setReady);
-            sinon.assert.calledWith(conversation.setReady, fakeSessionData);
-          });
+        it("should save the sessionData", function() {
+          conversation.outgoing(fakeSessionData);
 
-        it("should trigger a `session:error` event errno is undefined",
-          function(done) {
-            var errMsg = "HTTP 500 Server Error; fake";
-            var err = new Error(errMsg);
-            requestCallInfoStub.callsArgWith(2, err);
+          sinon.assert.calledOnce(conversation.setSessionData);
+        });
 
-            conversation.on("session:error", function(err) {
-              expect(err.message).eql(errMsg);
-              done();
-            }).initiate({ client: fakeClient, outgoing: true });
+        it("should trigger a `call:outgoing` event", function(done) {
+          conversation.once("call:outgoing", function() {
+            done();
           });
 
-        it("should trigger a `session:error` event when errno is not 105",
-          function(done) {
-            var errMsg = "HTTP 400 Bad Request; fake";
-            var err = new Error(errMsg);
-            err.errno = 101;
-            requestCallInfoStub.callsArgWith(2, err);
-
-            conversation.on("session:error", function(err) {
-              expect(err.message).eql(errMsg);
-              done();
-            }).initiate({ client: fakeClient, outgoing: true });
-          });
-
-        it("should trigger a `session:expired` event when errno is 105",
-          function(done) {
-            var err = new Error("HTTP 404 Not Found; fake");
-            err.errno = 105;
-            requestCallInfoStub.callsArgWith(2, err);
-
-            conversation.on("session:expired", function(err2) {
-              expect(err2).eql(err);
-              done();
-            }).initiate({ client: fakeClient, outgoing: true });
-          });
+          conversation.outgoing();
+        });
 
         it("should end the session on outgoing call timeout", function() {
-          requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
-
-          conversation.initiate({
-            client: fakeClient,
-            outgoing: true
-          });
+          conversation.outgoing();
 
           sandbox.clock.tick(1001);
 
           sinon.assert.calledOnce(conversation.endSession);
         });
 
         it("should trigger a `timeout` event on outgoing call timeout",
           function(done) {
-            requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
-
             conversation.once("timeout", function() {
               done();
             });
 
-            conversation.initiate({
-              client: fakeClient,
-              outgoing: true
-            });
+            conversation.outgoing();
 
             sandbox.clock.tick(1001);
           });
       });
 
-      describe("#setReady", function() {
+      describe("#setSessionData", function() {
         it("should update conversation session information", function() {
-          conversation.setReady(fakeSessionData);
+          conversation.setSessionData(fakeSessionData);
 
           expect(conversation.get("sessionId")).eql("sessionId");
           expect(conversation.get("sessionToken")).eql("sessionToken");
           expect(conversation.get("apiKey")).eql("apiKey");
         });
-
-        it("should trigger a `session:ready` event", function(done) {
-          conversation.on("session:ready", function() {
-            done();
-          }).setReady(fakeSessionData);
-        });
       });
 
       describe("#startSession", function() {
         var model;
 
         beforeEach(function() {
           sandbox.stub(sharedModels.ConversationModel.prototype,
                        "_clearPendingCallTimer");
--- a/browser/components/loop/test/shared/router_test.js
+++ b/browser/components/loop/test/shared/router_test.js
@@ -92,47 +92,52 @@ describe("loop.shared.router", function(
     });
   });
 
   describe("BaseConversationRouter", function() {
     var conversation, TestRouter;
 
     beforeEach(function() {
       TestRouter = loop.shared.router.BaseConversationRouter.extend({
-        startCall: sandbox.spy(),
         endCall: sandbox.spy()
       });
       conversation = new loop.shared.models.ConversationModel({
         loopToken: "fakeToken"
       }, {
         sdk: {},
         pendingCallTimeout: 1000
       });
     });
 
     describe("#constructor", function() {
       it("should require a ConversationModel instance", function() {
         expect(function() {
-          new TestRouter();
+          new TestRouter({ client: {} });
         }).to.Throw(Error, /missing required conversation/);
       });
+      it("should require a Client instance", function() {
+        expect(function() {
+          new TestRouter({ conversation: {} });
+        }).to.Throw(Error, /missing required client/);
+      });
     });
 
     describe("Events", function() {
       var router, fakeSessionData;
 
       beforeEach(function() {
         fakeSessionData = {
           sessionId:    "sessionId",
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
         router = new TestRouter({
           conversation: conversation,
-          notifier: notifier
+          notifier: notifier,
+          client: {}
         });
       });
 
       describe("session:connection-error", function() {
 
         it("should warn the user when .connect() call fails", function() {
           conversation.trigger("session:connection-error");
 
@@ -144,22 +149,16 @@ describe("loop.shared.router", function(
           conversation.trigger("session:connection-error");
 
           sinon.assert.calledOnce(router.endCall);
           sinon.assert.calledWithExactly(router.endCall);
         });
 
       });
 
-      it("should call startCall() once the call session is ready", function() {
-        conversation.trigger("session:ready");
-
-        sinon.assert.calledOnce(router.startCall);
-      });
-
       it("should call endCall() when conversation ended", function() {
         conversation.trigger("session:ended");
 
         sinon.assert.calledOnce(router.endCall);
       });
 
       it("should warn the user when peer hangs up", function() {
         conversation.trigger("session:peer-hungup");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -64,26 +64,31 @@ describe("loop.webapp", function() {
 
       sinon.assert.calledOnce(WebappRouter.prototype.navigate);
       sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
                                      "unsupportedBrowser", {trigger: true});
     });
   });
 
   describe("WebappRouter", function() {
-    var router, conversation;
+    var router, conversation, client;
 
     beforeEach(function() {
+      client = new loop.StandaloneClient({
+        baseServerUrl: "http://fake.example.com"
+      });
+      sandbox.stub(client, "requestCallInfo");
       conversation = new sharedModels.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000
       });
       sandbox.stub(loop.webapp.WebappRouter.prototype, "loadReactComponent");
       router = new loop.webapp.WebappRouter({
         helper: {},
+        client: client,
         conversation: conversation,
         notifier: notifier
       });
       sandbox.stub(router, "loadView");
       sandbox.stub(router, "navigate");
     });
 
     describe("#startCall", function() {
@@ -157,24 +162,24 @@ describe("loop.webapp", function() {
 
       describe("#initiate", function() {
         it("should set the token on the conversation model", function() {
           router.initiate("fakeToken");
 
           expect(conversation.get("loopToken")).eql("fakeToken");
         });
 
-        it("should load the ConversationFormView", function() {
+        it("should load the StartConversationView", function() {
           router.initiate("fakeToken");
 
           sinon.assert.calledOnce(router.loadReactComponent);
           sinon.assert.calledWithExactly(router.loadReactComponent,
             sinon.match(function(value) {
               return React.addons.TestUtils.isDescriptorOfType(
-                value, loop.webapp.ConversationFormView);
+                value, loop.webapp.StartConversationView);
             }));
         });
 
         // https://bugzilla.mozilla.org/show_bug.cgi?id=991118
         it("should terminate any ongoing call session", function() {
           sinon.stub(conversation, "endSession");
           conversation.set("ongoing", true);
 
@@ -237,17 +242,18 @@ describe("loop.webapp", function() {
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
         conversation.set("loopToken", "fakeToken");
       });
 
       it("should navigate to call/ongoing/:token once call session is ready",
         function() {
-          conversation.trigger("session:ready");
+          router.setupOutgoingCall();
+          conversation.outgoing(fakeSessionData);
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
         });
 
       it("should navigate to call/{token} when conversation ended", function() {
         conversation.trigger("session:ended");
 
@@ -265,27 +271,91 @@ describe("loop.webapp", function() {
       it("should navigate to call/{token} when network disconnects",
         function() {
           conversation.trigger("session:network-disconnected");
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
         });
 
-      it("should navigate to call/expired when a session:expired event is " +
-         "received", function() {
-        conversation.trigger("session:expired");
+      describe("#setupOutgoingCall", function() {
+        beforeEach(function() {
+          router.initiate();
+        });
+
+        describe("No loop token", function() {
+          it("should navigate to home", function() {
+            conversation.setupOutgoingCall();
+
+            sinon.assert.calledOnce(router.navigate);
+            sinon.assert.calledWithMatch(router.navigate, "home");
+          });
+
+          it("should display an error", function() {
+            conversation.setupOutgoingCall();
+
+            sinon.assert.calledOnce(notifier.errorL10n);
+          });
+        });
+
+        describe("Has loop token", function() {
+          beforeEach(function() {
+            conversation.set("loopToken", "fakeToken");
+            sandbox.stub(conversation, "outgoing");
+          });
+
+          it("should call requestCallInfo on the client",
+            function() {
+              conversation.setupOutgoingCall();
+
+              sinon.assert.calledOnce(client.requestCallInfo);
+              sinon.assert.calledWith(client.requestCallInfo, "fakeToken",
+                                      "audio-video");
+            });
 
-        sinon.assert.calledOnce(router.navigate);
-        sinon.assert.calledWith(router.navigate, "/call/expired");
+          describe("requestCallInfo response handling", function() {
+            it("should navigate to call/expired when a session has expired",
+               function() {
+                client.requestCallInfo.callsArgWith(2, {errno: 105});
+                conversation.setupOutgoingCall();
+
+                sinon.assert.calledOnce(router.navigate);
+                sinon.assert.calledWith(router.navigate, "/call/expired");
+              });
+
+            it("should navigate to home on any other error", function() {
+              client.requestCallInfo.callsArgWith(2, {errno: 104});
+              conversation.setupOutgoingCall();
+
+              sinon.assert.calledOnce(router.navigate);
+              sinon.assert.calledWith(router.navigate, "home");
+              });
+
+            it("should notify the user on any other error", function() {
+              client.requestCallInfo.callsArgWith(2, {errno: 104});
+              conversation.setupOutgoingCall();
+
+              sinon.assert.calledOnce(notifier.errorL10n);
+            });
+
+            it("should call outgoing on the conversation model when details " +
+               "are successfully received", function() {
+                client.requestCallInfo.callsArgWith(2, null, fakeSessionData);
+                conversation.setupOutgoingCall();
+
+                sinon.assert.calledOnce(conversation.outgoing);
+                sinon.assert.calledWithExactly(conversation.outgoing, fakeSessionData);
+              });
+          });
+        });
       });
     });
   });
 
-  describe("ConversationFormView", function() {
+  describe("StartConversationView", function() {
     var conversation;
 
     beforeEach(function() {
       conversation = new sharedModels.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000});
     });
 
@@ -293,52 +363,49 @@ describe("loop.webapp", function() {
       it("should require a conversation option", function() {
         expect(function() {
           new loop.webapp.WebappRouter();
         }).to.Throw(Error, /missing required conversation/);
       });
     });
 
     describe("#initiate", function() {
-      var conversation, initiate, view, fakeSubmitEvent, requestCallUrlInfo;
+      var conversation, setupOutgoingCall, view, fakeSubmitEvent,
+          requestCallUrlInfo;
 
       beforeEach(function() {
         conversation = new sharedModels.ConversationModel({}, {
           sdk: {},
           pendingCallTimeout: 1000
         });
 
         fakeSubmitEvent = {preventDefault: sinon.spy()};
-        initiate = sinon.stub(conversation, "initiate");
+        setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
 
         var standaloneClientStub = {
           requestCallUrlInfo: function(token, cb) {
             cb(null, {urlCreationDate: 0});
           },
           settings: {baseServerUrl: loop.webapp.baseServerUrl}
-        }
+        };
 
         view = React.addons.TestUtils.renderIntoDocument(
-            loop.webapp.ConversationFormView({
+            loop.webapp.StartConversationView({
               model: conversation,
               notifier: notifier,
               client: standaloneClientStub
             })
         );
       });
 
       it("should start the conversation establishment process", function() {
         var button = view.getDOMNode().querySelector("button");
         React.addons.TestUtils.Simulate.click(button);
 
-        sinon.assert.calledOnce(initiate);
-        sinon.assert.calledWith(initiate, sinon.match(function (value) {
-          return !!value.outgoing &&
-            (value.client.settings.baseServerUrl === loop.webapp.baseServerUrl)
-        }, "outgoing: true && correct baseServerUrl"));
+        sinon.assert.calledOnce(setupOutgoingCall);
       });
 
       it("should disable current form once session is initiated", function() {
         conversation.set("loopToken", "fake");
 
         var button = view.getDOMNode().querySelector("button");
         React.addons.TestUtils.Simulate.click(button);
 
@@ -368,17 +435,17 @@ describe("loop.webapp", function() {
           sdk: {},
           pendingCallTimeout: 1000
         });
 
         sandbox.spy(conversation, "listenTo");
         requestCallUrlInfo = sandbox.stub();
 
         view = React.addons.TestUtils.renderIntoDocument(
-            loop.webapp.ConversationFormView({
+            loop.webapp.StartConversationView({
               model: conversation,
               notifier: notifier,
               client: {requestCallUrlInfo: requestCallUrlInfo}
             })
           );
       });
 
       it("should call requestCallUrlInfo", function() {