diff --git a/package.json b/package.json
index 883fdae8d5..9ac765051b 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
     "matrix-js-sdk": "0.8.5",
     "optimist": "^0.6.1",
     "prop-types": "^15.5.8",
+    "querystring": "^0.2.0",
     "react": "^15.4.0",
     "react-addons-css-transition-group": "15.3.2",
     "react-dom": "^15.4.0",
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 0070af1fb2..b2fb1805ba 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -17,6 +17,7 @@ limitations under the License.
 'use strict';
 
 import url from 'url';
+import qs from 'querystring';
 import React from 'react';
 import MatrixClientPeg from '../../../MatrixClientPeg';
 import PlatformPeg from '../../../PlatformPeg';
@@ -51,42 +52,63 @@ export default React.createClass({
         creatorUserId: React.PropTypes.string,
     },
 
-    getDefaultProps: function() {
+    getDefaultProps() {
         return {
             url: "",
         };
     },
 
-    getInitialState: function() {
-        const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
+    /**
+     * Set initial component state when the App wUrl (widget URL) is being updated.
+     * Component props *must* be passed (rather than relying on this.props).
+     * @param  {Object} newProps The new properties of the component
+     * @return {Object} Updated component state to be set with setState
+     */
+    _getNewState(newProps) {
+        const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
         const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
         return {
-            loading: false,
-            widgetUrl: this.props.url,
+            initialising: true,   // True while we are mangling the widget URL
+            loading: true,        // True while the iframe content is loading
+            widgetUrl: newProps.url,
             widgetPermissionId: widgetPermissionId,
-            // Assume that widget has permission to load if we are the user who added it to the room, or if explicitly granted by the user
-            hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
+            // Assume that widget has permission to load if we are the user who
+            // added it to the room, or if explicitly granted by the user
+            hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
             error: null,
             deleting: false,
         };
     },
 
-    // Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
-    isScalarUrl: function() {
+    getInitialState() {
+        return this._getNewState(this.props);
+    },
+
+    /**
+     * Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
+     * @param  {[type]}  url URL to check
+     * @return {Boolean} True if specified URL is a scalar URL
+     */
+    isScalarUrl(url) {
+        if (!url) {
+            console.error('Scalar URL check failed. No URL specified');
+            return false;
+        }
+
         let scalarUrls = SdkConfig.get().integrations_widgets_urls;
         if (!scalarUrls || scalarUrls.length == 0) {
             scalarUrls = [SdkConfig.get().integrations_rest_url];
         }
 
         for (let i = 0; i < scalarUrls.length; i++) {
-            if (this.props.url.startsWith(scalarUrls[i])) {
+            if (url.startsWith(scalarUrls[i])) {
                 return true;
             }
         }
         return false;
     },
 
-    isMixedContent: function() {
+    isMixedContent() {
         const parentContentProtocol = window.location.protocol;
         const u = url.parse(this.props.url);
         const childContentProtocol = u.protocol;
@@ -98,43 +120,73 @@ export default React.createClass({
         return false;
     },
 
-    componentWillMount: function() {
-        if (!this.isScalarUrl()) {
+    componentWillMount() {
+        window.addEventListener('message', this._onMessage, false);
+        this.setScalarToken();
+    },
+
+    /**
+     * Adds a scalar token to the widget URL, if required
+     * Component initialisation is only complete when this function has resolved
+     */
+    setScalarToken() {
+        this.setState({initialising: true});
+
+        if (!this.isScalarUrl(this.props.url)) {
+            console.warn('Non-scalar widget, not setting scalar token!', url);
+            this.setState({
+                error: null,
+                widgetUrl: this.props.url,
+                initialising: false,
+            });
             return;
         }
-        // Fetch the token before loading the iframe as we need to mangle the URL
-        this.setState({
-            loading: true,
-        });
-        this._scalarClient = new ScalarAuthClient();
+
+        // Fetch the token before loading the iframe as we need it to mangle the URL
+        if (!this._scalarClient) {
+            this._scalarClient = new ScalarAuthClient();
+        }
         this._scalarClient.getScalarToken().done((token) => {
-            // Append scalar_token as a query param
+            // Append scalar_token as a query param if not already present
             this._scalarClient.scalarToken = token;
             const u = url.parse(this.props.url);
-            if (!u.search) {
-                u.search = "?scalar_token=" + encodeURIComponent(token);
-            } else {
-                u.search += "&scalar_token=" + encodeURIComponent(token);
+            const params = qs.parse(u.query);
+            if (!params.scalar_token) {
+                params.scalar_token = encodeURIComponent(token);
+                // u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
+                u.search = undefined;
+                u.query = params;
             }
 
             this.setState({
                 error: null,
                 widgetUrl: u.format(),
-                loading: false,
+                initialising: false,
             });
         }, (err) => {
+            console.error("Failed to get scalar_token", err);
             this.setState({
                 error: err.message,
-                loading: false,
+                initialising: false,
             });
         });
-        window.addEventListener('message', this._onMessage, false);
     },
 
     componentWillUnmount() {
         window.removeEventListener('message', this._onMessage);
     },
 
+    componentWillReceiveProps(nextProps) {
+        if (nextProps.url !== this.props.url) {
+            this._getNewState(nextProps);
+            this.setScalarToken();
+        } else if (nextProps.show && !this.props.show) {
+            this.setState({
+                loading: true,
+            });
+        }
+    },
+
     _onMessage(event) {
         if (this.props.type !== 'jitsi') {
             return;
@@ -154,11 +206,11 @@ export default React.createClass({
         }
     },
 
-    _canUserModify: function() {
+    _canUserModify() {
         return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
     },
 
-    _onEditClick: function(e) {
+    _onEditClick(e) {
         console.log("Edit widget ID ", this.props.id);
         const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
         const src = this._scalarClient.getScalarInterfaceUrlForRoom(
@@ -168,9 +220,10 @@ export default React.createClass({
         }, "mx_IntegrationsManager");
     },
 
-    /* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
+    /* If user has permission to modify widgets, delete the widget,
+     * otherwise revoke access for the widget to load in the user's browser
     */
-    _onDeleteClick: function() {
+    _onDeleteClick() {
         if (this._canUserModify()) {
             // Show delete confirmation dialog
             const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@@ -202,6 +255,10 @@ export default React.createClass({
         }
     },
 
+    _onLoaded() {
+        this.setState({loading: false});
+    },
+
     // Widget labels to render, depending upon user permissions
     // These strings are translated at the point that they are inserted in to the DOM, in the render method
     _deleteWidgetLabel() {
@@ -224,7 +281,7 @@ export default React.createClass({
         this.setState({hasPermissionToLoad: false});
     },
 
-    formatAppTileName: function() {
+    formatAppTileName() {
         let appTileName = "No name";
         if(this.props.name && this.props.name.trim()) {
             appTileName = this.props.name.trim();
@@ -232,7 +289,7 @@ export default React.createClass({
         return appTileName;
     },
 
-    onClickMenuBar: function(ev) {
+    onClickMenuBar(ev) {
         ev.preventDefault();
 
         // Ignore clicks on menu bar children
@@ -247,7 +304,7 @@ export default React.createClass({
         });
     },
 
-    render: function() {
+    render() {
         let appTileBody;
 
         // Don't render widget if it is in the process of being deleted
@@ -269,29 +326,30 @@ export default React.createClass({
         }
 
         if (this.props.show) {
-            if (this.state.loading) {
-                appTileBody = (
-                    <div className='mx_AppTileBody mx_AppLoading'>
-                        <MessageSpinner msg='Loading...' />
-                    </div>
-                );
+            const loadingElement = (
+                <div className='mx_AppTileBody mx_AppLoading'>
+                    <MessageSpinner msg='Loading...' />
+                </div>
+            );
+            if (this.state.initialising) {
+                appTileBody = loadingElement;
             } else if (this.state.hasPermissionToLoad == true) {
                 if (this.isMixedContent()) {
                     appTileBody = (
                         <div className="mx_AppTileBody">
-                            <AppWarning
-                                errorMsg="Error - Mixed content"
-                            />
+                            <AppWarning errorMsg="Error - Mixed content" />
                         </div>
                     );
                 } else {
                     appTileBody = (
-                        <div className="mx_AppTileBody">
+                        <div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
+                            { this.state.loading && loadingElement }
                             <iframe
                                 ref="appFrame"
                                 src={safeWidgetUrl}
                                 allowFullScreen="true"
                                 sandbox={sandboxFlags}
+                                onLoad={this._onLoaded}
                             ></iframe>
                         </div>
                     );