diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss
index 08f3144d61d0b8c5ccf24057cc67a404431417a0..04f15e84f90df3851181ea9b1362016ae0d5ebf3 100644
--- a/src/assets/stylesheets/entry.scss
+++ b/src/assets/stylesheets/entry.scss
@@ -10,15 +10,19 @@
   :local(.collapse) {
     @extend %fa-icon-button;
     position: absolute;
-    top: 4px;
-    right: 24px;
+    top: 0px;
+    right: 12px;
+    width: 32px;
+    height: 32px;
   }
 
   :local(.expand) {
     @extend %fa-icon-button;
     position: absolute;
-    top: -32px;
-    right: 24px;
+    top: -48px;
+    right: 12px;
+    width: 32px;
+    height: 32px;
   }
 }
 
@@ -86,6 +90,7 @@
 
   :local(.profile-name) {
     margin-top: 4px;
+    margin-bottom: 16px;
     @extend %default-font;
     cursor: pointer;
     font-weight: bold;
@@ -107,6 +112,7 @@
     flex-direction: column;
     height: 100%;
     justify-content: flex-end;
+    align-items: center;
 
     :local(.action-button) {
       @extend %action-button;
diff --git a/src/assets/stylesheets/hub.scss b/src/assets/stylesheets/hub.scss
index e24bf7f06dd6c27231971ce03e244ac829087d7c..8ce382b26542abc5df4cc138c0846035c700f81a 100644
--- a/src/assets/stylesheets/hub.scss
+++ b/src/assets/stylesheets/hub.scss
@@ -13,11 +13,14 @@
   display: none;
 }
 
+.grab-cursor {
+  cursor: grab;
+}
+
 .no-cursor {
   cursor: none;
 }
 
-
 .webxr-realities, .webxr-sessions {
   @extend %unselectable
 }
diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss
index 27ea17a7683c90b7d522bb77f3e635f7fadec0da..6e82b74501cd5c142262a90dae6d5900fc6b4dd6 100644
--- a/src/assets/stylesheets/info-dialog.scss
+++ b/src/assets/stylesheets/info-dialog.scss
@@ -62,7 +62,7 @@
 
       &__close {
         position: absolute;
-        left: 12px;
+        right: 12px;
         top: 6px;
         color: white;
         font-size: 1.4em;
diff --git a/src/assets/stylesheets/invite-dialog.scss b/src/assets/stylesheets/invite-dialog.scss
new file mode 100644
index 0000000000000000000000000000000000000000..cd4bb07e15364013160c98c024a33f99a770b8a3
--- /dev/null
+++ b/src/assets/stylesheets/invite-dialog.scss
@@ -0,0 +1,99 @@
+@import 'shared.scss';
+
+:local(.dialog) {
+  background-color: $action-color;
+  border-radius: 12px;
+  box-shadow: 0px 5px 30px 1px #333;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 10px;
+  padding: 14px;
+  text-align: center;
+  position: relative;
+  font-size: 1.1em;
+  
+  a {
+    color: white;
+    text-decoration: underline;
+    font-weight: bold;
+  }
+}
+
+:local(.close) {
+  position: absolute;
+  width: 30px;
+  height: 30px;
+  right: 12px;
+  font-size: 2.0em;
+  top: 0px;
+  cursor: pointer;
+}
+
+:local(.attach-point) {
+  width: 0; 
+  height: 0; 
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-bottom: 5px solid $action-color;
+  position: absolute;
+  top: -5px;
+}
+
+:local(.code) {
+  color: black;
+  font-family: monospace;
+  font-weight: bold;
+  text-decoration: none;
+  font-size: 2.0em;
+  display: flex;
+  margin: 12px;
+}
+
+:local(.header) {
+  font-weight: bold;
+}
+
+:local(.keep-open) {
+  font-size: 0.8em;
+}
+
+:local(.domain) {
+  input {
+    @extend %default-font;
+    font-weight: bold;
+    text-decoration: none;
+    color: black;
+    text-align: center;
+    background-color: white;
+    border: 1px solid #e2e2e2;
+    border-radius: 12px;
+    margin: 12px;
+    font-size: 1.8em;
+    padding: 14px;
+    display: block;
+    width: 295px;
+  }
+}
+
+:local(.copy-link-button) {
+  @extend %action-button-selected;
+  margin-top: 4px;
+}
+
+:local(.digit) {
+  padding: 0 8px;
+  margin: 2px;
+  background-color: white;
+  border: 1px solid #e2e2e2;
+  border-radius: 12px;
+  width: 32px;
+  height: 64px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+:local(.code-loading-panel) {
+  background: none;
+}
diff --git a/src/assets/stylesheets/link-dialog.scss b/src/assets/stylesheets/link-dialog.scss
deleted file mode 100644
index aafcac281e33a362fc69871f9474fff32ddd73b7..0000000000000000000000000000000000000000
--- a/src/assets/stylesheets/link-dialog.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-:local(.domain) , :local(.code) {
-  color: white;
-  font-family: monospace;
-  font-weight: bold;
-  text-decoration: none;
-}
-
-:local(.domain) {
-  font-size: 3em;
-  padding: 14px;
-  display: block;
-}
-
-:local(.code) {
-  font-size: 4.0em;
-  padding: 8px;
-}
-
-:local(.keep-open) {
-  font-size: 0.8em;
-}
-
-:local(.digit) {
-  padding: 0 8px;
-}
-
-:local(.code-loading-panel) {
-  background: none;
-}
diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss
index b94325c5039062d7848a4d254df9d27b88a7ceb4..accfb2be29a363c1022e558a31a9993d6acf92b8 100644
--- a/src/assets/stylesheets/link.scss
+++ b/src/assets/stylesheets/link.scss
@@ -169,7 +169,7 @@ a {
   background: transparent;
   color: white;
   margin: 0;
-  font-size: 64pt;
+  font-size: 52pt;
   border: 0;
   width: 295px;
   letter-spacing: 0.08em;
diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss
index be3161f6f8b20bef71e3c65245517fe6c743ed8e..5ebac8182d8ef8d71b7430849ebb0231eea781c8 100644
--- a/src/assets/stylesheets/profile.scss
+++ b/src/assets/stylesheets/profile.scss
@@ -14,7 +14,7 @@
   background-color: rgba(255, 255, 255, 0.9);
 
   :local(.logo) {
-    width: 100px;
+    width: 150px;
     position: absolute;
     right: 32px;
     bottom: 32px;
@@ -71,7 +71,7 @@
 
       a {
         margin: 0px 12px;
-        color: $grey-text;
+        color: $dark-grey-text;
       }
     }
 
diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss
index 44155ae2ed6e7f48ea4ce9d78fecca10a1859d99..e81425cfe23065ac4316cfe52cd433e429dd550b 100644
--- a/src/assets/stylesheets/shared.scss
+++ b/src/assets/stylesheets/shared.scss
@@ -3,12 +3,13 @@ $dark-transparent: rgba(0, 0, 0, 0.4);
 $darker-transparent: rgba(0, 0, 0, 0.6);
 $darkest-transparent: rgba(0, 0, 0, 0.9);
 $grey-text: rgba(192, 192, 192, 1.0);
+$dark-grey-text: rgba(64, 64, 64, 1.0);
 $light-text: rgba(240, 240, 240, 1.0);
 $light-grey: lightgrey;
 $dark-grey: rgba(128, 128, 128, 1.0);
 $darker-grey: rgba(64, 64, 64, 1.0);
 $darkest-grey: rgba(32, 32, 32, 1.0);
-$action-text: #2F80ED;
+$action-color: #2F80ED;
 
 %unselectable {
   -moz-user-select: none;
@@ -96,6 +97,11 @@ $action-text: #2F80ED;
   min-width: 150px;
 }
 
+%action-button-selected {
+  background: white;
+  color: $action-color;
+}
+
 %bottom-action-button {
   @extend %bottom-button;
   background: #2F80ED;
diff --git a/src/assets/stylesheets/ui-root.scss b/src/assets/stylesheets/ui-root.scss
index 2ceeacef10fa9224b02c58165197ae9ee3a37292..0de53b2874508c94b1a66260369a95de05e2971a 100644
--- a/src/assets/stylesheets/ui-root.scss
+++ b/src/assets/stylesheets/ui-root.scss
@@ -71,7 +71,7 @@
   @extend %unselectable;
 }
 
-:local(.nag-button) {
+:local(.invite-container) {
   @extend %unselectable;
   position: absolute;
   top: 0;
@@ -79,9 +79,10 @@
   margin-top: 16px;
   width: 100%;
   display: flex;
+  flex-direction: column;
   align-items: center;
   justify-content: center;
-  pointer-events: none;
+  pointer-events: auto;
 
   button {
     @extend %action-button;
@@ -95,7 +96,13 @@
   }
 }
 
-:local(.nag-button-below-hud) {
+:local(.invite-container-inverted) {
+  button {
+    @extend %action-button-selected;
+  }
+}
+
+:local(.invite-container-below-hud) {
   margin-top: 100px;
 }
 
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 53ee3718731361912f71b82454a794c04a5087f1..a94df090766c24d16ebf686ae03b59476e42bb55 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -74,15 +74,18 @@
     "home.have_entry_code": "have a link code?",
     "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in",
     "mailing_list.privacy_link": "this Privacy Notice",
-    "link.in_your_browser": "In your headset's browser, go to:",
-    "link.enter_code": "Then, enter this link code:",
-    "link.do_not_close": "Keep this dialog open to use this code.",
     "link.link_page_header": "Enter your code:",
     "link.dont_have_a_code": "Don't have a code?",
     "link.create_a_room": "Create a Room",
     "link.try_again": "We couldn't find that code. Please try again.",
     "help.report_issue": "Report an Issue",
     "scene.logo_tagline": "A new way to get together",
-    "scene.create_button": "create a room with this scene"
+    "scene.create_button": "create a room with this scene",
+    "invite.in_your_browser": "In your headset's browser, go to:",
+    "invite.entry_code": "Entry Code:",
+    "invite.and_enter_code": "and enter code:",
+    "invite.join_at": "Join room at ",
+    "invite.direct_link": "Direct Link:",
+    "invite.enter_in_browser": "Enter in browser:"
   }
 }
diff --git a/src/hub.html b/src/hub.html
index 19a8282eab596478712464d1a9bef2fb95be8a8b..16dd5bdade66eef08ede23d13c7e104b49847882 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -21,6 +21,7 @@
     </audio>
 
     <a-scene
+        class="grab-cursor"
         renderer="antialias: true; gammaOutput: true; sortObjects: true; physicallyCorrectLights: true;"
         gamma-factor
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
diff --git a/src/hub.js b/src/hub.js
index f7b2717741233ac5556f752fbb33a0841b1865e6..e397164f5ad63a6a3f6f97141f2dd4ab0632c171 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -73,7 +73,6 @@ import ReactDOM from "react-dom";
 import React from "react";
 import UIRoot from "./react-components/ui-root";
 import HubChannel from "./utils/hub-channel";
-import LinkChannel from "./utils/link-channel";
 import { connectToReticulum } from "./utils/phoenix-utils";
 import { disableiOSZoom } from "./utils/disable-ios-zoom";
 import { resolveMedia } from "./utils/media-utils";
@@ -252,7 +251,7 @@ async function handleHubChannelJoined(entryManager, hubChannel, data) {
     environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
   }
 
-  remountUI({ hubId: hub.hub_id, hubName: hub.name });
+  remountUI({ hubId: hub.hub_id, hubName: hub.name, hubEntryCode: hub.entry_code });
 
   scene.setAttribute("networked-scene", {
     room: hub.hub_id,
@@ -294,7 +293,7 @@ document.addEventListener("DOMContentLoaded", () => {
   const scene = document.querySelector("a-scene");
   const hubChannel = new HubChannel(store);
   const entryManager = new SceneEntryManager(hubChannel);
-  const linkChannel = new LinkChannel(store);
+  entryManager.init();
 
   window.APP.scene = scene;
 
@@ -384,6 +383,4 @@ document.addEventListener("DOMContentLoaded", () => {
     if (!NAF.connection.adapter) return;
     NAF.connection.adapter.onData(data);
   });
-
-  linkChannel.setSocket(socket);
 });
diff --git a/src/link.js b/src/link.js
index 401fe54d9b8b9bd91df1c2140257710502a23add..908f7f64d4fadd8f906b09a7874e8e882c93b81d 100644
--- a/src/link.js
+++ b/src/link.js
@@ -3,18 +3,7 @@ import React from "react";
 import ReactDOM from "react-dom";
 import registerTelemetry from "./telemetry";
 import LinkRoot from "./react-components/link-root";
-import LinkChannel from "./utils/link-channel";
-import { connectToReticulum } from "./utils/phoenix-utils";
-import Store from "./storage/store";
 
 registerTelemetry();
 
-const socket = connectToReticulum();
-const store = new Store();
-store.init();
-
-const linkChannel = new LinkChannel(store);
-
-linkChannel.setSocket(socket);
-
-ReactDOM.render(<LinkRoot store={store} linkChannel={linkChannel} />, document.getElementById("link-root"));
+ReactDOM.render(<LinkRoot />, document.getElementById("link-root"));
diff --git a/src/react-components/invite-dialog.js b/src/react-components/invite-dialog.js
index e01fc1282cf835e8e5a65363b3dfc6ee0f4e891f..fce6863c845227dc3c5cd9a89fc7c3c5add2ba78 100644
--- a/src/react-components/invite-dialog.js
+++ b/src/react-components/invite-dialog.js
@@ -1,56 +1,101 @@
 import React, { Component } from "react";
+import PropTypes from "prop-types";
 import copy from "copy-to-clipboard";
-import DialogContainer from "./dialog-container.js";
+import { FormattedMessage } from "react-intl";
+
+import styles from "../assets/stylesheets/invite-dialog.scss";
+
+function pad(num, size) {
+  let s = `${num}`;
+  while (s.length < size) s = `0${s}`;
+  return s;
+}
 
 export default class InviteDialog extends Component {
+  static propTypes = {
+    entryCode: PropTypes.number,
+    dialogType: PropTypes.string,
+    onClose: PropTypes.func
+  };
+
   state = {
     copyLinkButtonText: "copy"
   };
 
-  constructor(props) {
-    super(props);
-    const loc = document.location;
-    this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`;
-  }
-
   copyLinkClicked = link => {
     copy(link);
     this.setState({ copyLinkButtonText: "copied!" });
   };
 
-  shareLinkClicked = () => {
-    navigator.share({
-      title: document.title,
-      url: this.shareLink
-    });
-  };
-
   render() {
-    return (
-      <DialogContainer title="Invite Others" {...this.props}>
-        <div>
-          <div>Just share the link and they&apos;ll join you:</div>
-          <div className="invite-form">
-            <input
-              type="text"
-              readOnly
-              onFocus={e => e.target.select()}
-              value={this.shareLink}
-              className="invite-form__link_field"
-            />
-            <div className="invite-form__buttons">
-              {navigator.share && (
-                <button className="invite-form__action-button" onClick={this.shareLinkClicked}>
-                  <span>share</span>
-                </button>
-              )}
-              <button className="invite-form__action-button" onClick={this.copyLinkClicked.bind(this, this.shareLink)}>
-                <span>{this.state.copyLinkButtonText}</span>
-              </button>
-            </div>
+    const { entryCode } = this.props;
+
+    const entryCodeString = pad(entryCode, 6);
+    const shareLink = `hub.link/${entryCodeString}`;
+    const isHeadsetLink = this.props.dialogType === "headset";
+
+    if (isHeadsetLink) {
+      return (
+        <div className={styles.dialog}>
+          <div className={styles.attachPoint} />
+          <div className={styles.close} onClick={() => this.props.onClose()}>
+            <span>×</span>
+          </div>
+          <div>
+            <FormattedMessage id="invite.in_your_browser" />
+          </div>
+          <div className={styles.domain}>
+            <input type="text" readOnly onFocus={e => e.target.select()} value="hub.link" />
+          </div>
+          <div>
+            <FormattedMessage id="invite.and_enter_code" />
+          </div>
+          <div className={styles.code}>
+            {entryCodeString.split("").map((d, i) => (
+              <div className={styles.digit} key={`link_code_${i}`}>
+                {d}
+              </div>
+            ))}
+          </div>
+        </div>
+      );
+    } else {
+      return (
+        <div className={styles.dialog}>
+          <div className={styles.attachPoint} />
+          <div className={styles.close} onClick={() => this.props.onClose()}>
+            <span>×</span>
+          </div>
+          <div className={styles.header}>
+            <FormattedMessage id="invite.entry_code" />
+          </div>
+          <div>
+            <FormattedMessage id="invite.join_at" />
+            <a href="https://hub.link" target="_blank" rel="noopener noreferrer">
+              hub.link
+            </a>
+          </div>
+          <div className={styles.code}>
+            {entryCodeString.split("").map((d, i) => (
+              <div className={styles.digit} key={`link_code_${i}`}>
+                {d}
+              </div>
+            ))}
+          </div>
+          <div className={styles.header} style={{ marginTop: "16px" }}>
+            <FormattedMessage id="invite.direct_link" />
+          </div>
+          <div>
+            <FormattedMessage id="invite.enter_in_browser" />
+          </div>
+          <div className={styles.domain}>
+            <input type="text" readOnly onFocus={e => e.target.select()} value={shareLink} />
           </div>
+          <button className={styles.copyLinkButton} onClick={this.copyLinkClicked.bind(this, "https://" + shareLink)}>
+            <span>{this.state.copyLinkButtonText}</span>
+          </button>
         </div>
-      </DialogContainer>
-    );
+      );
+    }
   }
 }
diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js
deleted file mode 100644
index 456090494e4cc9e00799d697530adbe460ae4bd2..0000000000000000000000000000000000000000
--- a/src/react-components/link-dialog.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import classNames from "classnames";
-import { FormattedMessage } from "react-intl";
-import DialogContainer from "./dialog-container.js";
-
-import styles from "../assets/stylesheets/link-dialog.scss";
-
-export default class LinkDialog extends Component {
-  static propTypes = {
-    linkCode: PropTypes.string
-  };
-
-  render() {
-    const { linkCode, ...other } = this.props;
-    if (!linkCode) {
-      return (
-        <DialogContainer title="Open on Headset" {...other}>
-          <div>
-            <div className={classNames("loading-panel", styles.codeLoadingPanel)}>
-              <div className="loader-wrap">
-                <div className="loader">
-                  <div className="loader-center" />
-                </div>
-              </div>
-            </div>
-          </div>
-        </DialogContainer>
-      );
-    }
-
-    return (
-      <DialogContainer title="Open on Headset" {...other}>
-        <div>
-          <div>
-            <FormattedMessage id="link.in_your_browser" />
-          </div>
-          <a href="https://hub.link" className={styles.domain} target="_blank" rel="noopener noreferrer">
-            hub.link
-          </a>
-          <div>
-            <FormattedMessage id="link.enter_code" />
-          </div>
-          <div className={styles.code}>
-            {linkCode.split("").map((d, i) => (
-              <span className={styles.digit} key={`link_code_${i}`}>
-                {d}
-              </span>
-            ))}
-          </div>
-          <div className={styles.keepOpen}>
-            <FormattedMessage id="link.do_not_close" />
-          </div>
-        </div>
-      </DialogContainer>
-    );
-  }
-}
diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js
index 84b7101637357253da095a0c69bd0f20382318e4..f9aab014bb2f0ba5e6779a4c1020d34aa8416f96 100644
--- a/src/react-components/link-root.js
+++ b/src/react-components/link-root.js
@@ -8,16 +8,14 @@ import classNames from "classnames";
 import styles from "../assets/stylesheets/link.scss";
 import { disableiOSZoom } from "../utils/disable-ios-zoom";
 
-const MAX_DIGITS = 4;
+const MAX_DIGITS = 6;
 
 addLocaleData([...en]);
 disableiOSZoom();
 
 class LinkRoot extends Component {
   static propTypes = {
-    intl: PropTypes.object,
-    store: PropTypes.object,
-    linkChannel: PropTypes.object
+    intl: PropTypes.object
   };
 
   state = {
@@ -62,30 +60,15 @@ class LinkRoot extends Component {
     this.setState({ enteredDigits: enteredDigits.substring(0, enteredDigits.length - 1) });
   };
 
-  attemptLink = code => {
-    this.props.linkChannel
-      .attemptLink(code)
-      .then(response => {
-        // If there is a profile from the linked device, copy it over if we don't have one yet.
-        if (response.profile) {
-          const { hasChangedName } = this.props.store.state.activity;
-
-          if (!hasChangedName) {
-            this.props.store.update({ activity: { hasChangedName: true }, profile: response.profile });
-          }
-        }
-
-        if (response.path) {
-          window.location.href = response.path;
-        }
-      })
-      .catch(e => {
-        this.setState({ failedAtLeastOnce: true, enteredDigits: "" });
-
-        if (!(e instanceof Error && (e.message === "in_use" || e.message === "failed"))) {
-          throw e;
-        }
-      });
+  attemptLink = async code => {
+    const url = "https://hub.link/" + code;
+    const res = await fetch(url);
+
+    if (res.status >= 400) {
+      this.setState({ failedAtLeastOnce: true, enteredDigits: "" });
+    } else {
+      document.location = url;
+    }
   };
 
   render() {
@@ -119,7 +102,7 @@ class LinkRoot extends Component {
                   onChange={ev => {
                     this.setState({ enteredDigits: ev.target.value });
                   }}
-                  placeholder="- - - -"
+                  placeholder="- - - - - -"
                 />
               </div>
 
diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js
index 9beb6e7a4cff70e2083ac012df120d8f0a7b0e18..307ae73b0dac97d5b50908d5773f28bc2953aec1 100644
--- a/src/react-components/profile-entry-panel.js
+++ b/src/react-components/profile-entry-panel.js
@@ -81,6 +81,18 @@ class ProfileEntryPanel extends Component {
             <label htmlFor="#profile-entry-display-name" className={styles.title}>
               <FormattedMessage id="profile.header" />
             </label>
+            <input
+              id="profile-entry-display-name"
+              className={styles.formFieldText}
+              value={this.state.displayName}
+              onFocus={e => e.target.select()}
+              onChange={e => this.setState({ displayName: e.target.value })}
+              required
+              spellCheck="false"
+              pattern={SCHEMA.definitions.profile.properties.displayName.pattern}
+              title={formatMessage({ id: "profile.display_name.validation_warning" })}
+              ref={inp => (this.nameInput = inp)}
+            />
             <div className={styles.avatarSelectorContainer}>
               <div className="loading-panel">
                 <div className="loader-wrap">
@@ -95,18 +107,6 @@ class ProfileEntryPanel extends Component {
                 ref={ifr => (this.avatarSelector = ifr)}
               />
             </div>
-            <input
-              id="profile-entry-display-name"
-              className={styles.formFieldText}
-              value={this.state.displayName}
-              onFocus={e => e.target.select()}
-              onChange={e => this.setState({ displayName: e.target.value })}
-              required
-              spellCheck="false"
-              pattern={SCHEMA.definitions.profile.properties.displayName.pattern}
-              title={formatMessage({ id: "profile.display_name.validation_warning" })}
-              ref={inp => (this.nameInput = inp)}
-            />
             <input className={styles.formSubmit} type="submit" value={formatMessage({ id: "profile.save" })} />
             <div className={styles.links}>
               <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md">
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 0c3fa5e8d463aa68b88c5736b358a13654e36fbc..e08bad5d3bc4bcbb3b2f28071e01f935793f6729 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -22,9 +22,8 @@ import ProfileEntryPanel from "./profile-entry-panel";
 import HelpDialog from "./help-dialog.js";
 import SafariDialog from "./safari-dialog.js";
 import WebVRRecommendDialog from "./webvr-recommend-dialog.js";
-import InviteDialog from "./invite-dialog.js";
 import InviteTeamDialog from "./invite-team-dialog.js";
-import LinkDialog from "./link-dialog.js";
+import InviteDialog from "./invite-dialog.js";
 import CreateObjectDialog from "./create-object-dialog.js";
 import TwoDHUD from "./2d-hud";
 import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers";
@@ -74,7 +73,7 @@ class UIRoot extends Component {
     store: PropTypes.object,
     scene: PropTypes.object,
     hubChannel: PropTypes.object,
-    linkChannel: PropTypes.object,
+    hubEntryCode: PropTypes.number,
     showProfileEntry: PropTypes.bool,
     availableVREntryTypes: PropTypes.object,
     environmentSceneLoaded: PropTypes.bool,
@@ -90,8 +89,7 @@ class UIRoot extends Component {
     entryStep: ENTRY_STEPS.start,
     enterInVR: false,
     dialog: null,
-    linkCode: null,
-    linkCodeCancel: null,
+    inviteDialogType: null,
 
     shareScreen: false,
     requestedScreen: false,
@@ -141,11 +139,7 @@ class UIRoot extends Component {
     this.props.scene.removeEventListener("exit", this.exit);
   }
 
-  componentDidUpdate(prevProps) {
-    if (this.props.availableVREntryTypes && prevProps.availableVREntryTypes !== this.props.availableVREntryTypes) {
-      this.handleForcedVREntryType();
-    }
-  }
+  componentDidUpdate(prevProps) {}
 
   onSceneLoaded = () => {
     this.setState({ sceneLoaded: true });
@@ -175,10 +169,10 @@ class UIRoot extends Component {
     this.props.scene.emit("spawn_pen");
   };
 
-  handleForcedVREntryType = () => {
-    if (!this.props.forcedVREntryType) return;
-
-    if (this.props.forcedVREntryType.startsWith("daydream")) {
+  handleStartEntry = () => {
+    if (!this.props.forcedVREntryType) {
+      this.setState({ entryStep: ENTRY_STEPS.device });
+    } else if (this.props.forcedVREntryType.startsWith("daydream")) {
       this.enterDaydream();
     } else if (this.props.forcedVREntryType.startsWith("vr")) {
       this.enterVR();
@@ -506,12 +500,16 @@ class UIRoot extends Component {
     this.setState({ entryStep: ENTRY_STEPS.finished });
   };
 
-  attemptLink = async () => {
-    this.showLinkDialog();
-    const { code, cancel, onFinished } = await this.props.linkChannel.generateCode();
-    this.setState({ linkCode: code, linkCodeCancel: cancel });
-    this.showLinkDialog();
-    onFinished.then(this.closeDialog);
+  showInviteDialog = async forHeadset => {
+    this.setState({ inviteDialogType: forHeadset ? "headset" : "invite" });
+  };
+
+  toggleInviteDialog = async () => {
+    if (this.state.inviteDialogType) {
+      this.setState({ inviteDialogType: null });
+    } else {
+      this.showInviteDialog(false);
+    }
   };
 
   closeDialog = async () => {
@@ -534,10 +532,6 @@ class UIRoot extends Component {
     this.setState({ dialog: <SafariDialog onClose={this.closeDialog} /> });
   }
 
-  showInviteDialog() {
-    this.setState({ dialog: <InviteDialog onClose={this.closeDialog} /> });
-  }
-
   showInviteTeamDialog() {
     this.setState({ dialog: <InviteTeamDialog hubChannel={this.props.hubChannel} onClose={this.closeDialog} /> });
   }
@@ -546,10 +540,6 @@ class UIRoot extends Component {
     this.setState({ dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} /> });
   }
 
-  showLinkDialog() {
-    this.setState({ dialog: <LinkDialog linkCode={this.state.linkCode} onClose={this.closeDialog} /> });
-  }
-
   showWebVRRecommendDialog() {
     this.setState({ dialog: <WebVRRecommendDialog onClose={this.closeDialog} /> });
   }
@@ -644,10 +634,7 @@ class UIRoot extends Component {
   renderEntryStartPanel = () => {
     return (
       <div className={entryStyles.entryPanel}>
-        <div className={entryStyles.title}>
-          {this.props.hubName}&apos;s&nbsp;
-          <FormattedMessage id="entry.enter-room-title" />
-        </div>
+        <div className={entryStyles.title}>{this.props.hubName}</div>
 
         <div className={entryStyles.center}>
           <div onClick={() => this.setState({ showProfileEntry: true })} className={entryStyles.profileName}>
@@ -659,7 +646,7 @@ class UIRoot extends Component {
         <div className={entryStyles.buttonContainer}>
           <button
             className={classNames([entryStyles.actionButton, entryStyles.wideButton])}
-            onClick={() => this.setState({ entryStep: ENTRY_STEPS.device })}
+            onClick={() => this.handleStartEntry()}
           >
             <FormattedMessage id="entry.enter-room" />
           </button>
@@ -695,7 +682,10 @@ class UIRoot extends Component {
           {this.props.availableVREntryTypes.daydream === VR_DEVICE_AVAILABILITY.yes && (
             <DaydreamEntryButton onClick={this.enterDaydream} subtitle={null} />
           )}
-          <DeviceEntryButton onClick={this.attemptLink} isInHMD={this.props.availableVREntryTypes.isInHMD} />
+          <DeviceEntryButton
+            onClick={() => this.showInviteDialog(true)}
+            isInHMD={this.props.availableVREntryTypes.isInHMD}
+          />
           {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
             <div className={entryStyles.secondary} onClick={this.enterVR}>
               <FormattedMessage id="entry.cardboard" />
@@ -932,17 +922,6 @@ class UIRoot extends Component {
         <div className={styles.ui}>
           {this.state.dialog}
 
-          <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}>
-            <i>
-              <FontAwesomeIcon icon={faQuestion} />
-            </i>
-          </button>
-
-          <div className={styles.presenceInfo}>
-            <FontAwesomeIcon icon={faUsers} />
-            <span className={styles.occupantCount}>{this.props.occupantCount || "-"}</span>
-          </div>
-
           {this.state.showProfileEntry && (
             <ProfileEntryPanel finished={this.onProfileFinished} store={this.props.store} />
           )}
@@ -953,14 +932,45 @@ class UIRoot extends Component {
             </div>
           )}
 
-          {!this.props.availableVREntryTypes.isInHMD &&
-            (!entryFinished || this.props.occupantCount <= 1) && (
-              <div className={classNames({ [styles.nagButton]: true, [styles.nagButtonBelowHud]: entryFinished })}>
-                <button onClick={() => this.showInviteDialog()}>
+          <div
+            className={classNames({
+              [styles.inviteContainer]: true,
+              [styles.inviteContainerBelowHud]: entryFinished,
+              [styles.inviteContainerInverted]: this.state.inviteDialogType
+            })}
+          >
+            {!this.props.availableVREntryTypes.isInHMD &&
+              (!entryFinished || this.props.occupantCount <= 1) && (
+                <button onClick={() => this.toggleInviteDialog()}>
                   <FormattedMessage id="entry.invite-others-nag" />
                 </button>
-              </div>
+              )}
+            {this.props.availableVREntryTypes.isInHMD &&
+              entryFinished && (
+                <button onClick={() => this.props.scene.enterVR()}>
+                  <FormattedMessage id="entry.return-to-vr" />
+                </button>
+              )}
+            {this.state.inviteDialogType && (
+              <InviteDialog
+                entryCode={this.props.hubEntryCode}
+                dialogType={this.state.inviteDialogType}
+                onClose={() => this.setState({ inviteDialogType: null })}
+              />
             )}
+          </div>
+
+          <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}>
+            <i>
+              <FontAwesomeIcon icon={faQuestion} />
+            </i>
+          </button>
+
+          <div className={styles.presenceInfo}>
+            <FontAwesomeIcon icon={faUsers} />
+            <span className={styles.occupantCount}>{this.props.occupantCount || "-"}</span>
+          </div>
+
           {this.state.entryStep === ENTRY_STEPS.finished ? (
             <div>
               <TwoDHUD.TopHUD
@@ -973,13 +983,6 @@ class UIRoot extends Component {
                 onSpawnPen={this.spawnPen}
                 onSpawnCamera={() => this.props.scene.emit("action_spawn_camera")}
               />
-              {this.props.availableVREntryTypes.isInHMD && (
-                <div className={styles.nagButton}>
-                  <button onClick={() => this.props.scene.enterVR()}>
-                    <FormattedMessage id="entry.return-to-vr" />
-                  </button>
-                </div>
-              )}
               {this.props.isSupportAvailable && (
                 <div className={styles.nagCornerButton}>
                   <button onClick={() => this.showInviteTeamDialog()}>
diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js
index e89de2b5247155a538926528ab4fd9f419b3d196..f5b65c1fdb3234391cae1220c34a574293c0a742 100644
--- a/src/scene-entry-manager.js
+++ b/src/scene-entry-manager.js
@@ -21,9 +21,16 @@ export default class SceneEntryManager {
     this.hubChannel = hubChannel;
     this.store = window.APP.store;
     this.scene = document.querySelector("a-scene");
+    this.cursorController = document.querySelector("#cursor-controller");
     this.playerRig = document.querySelector("#player-rig");
   }
 
+  init = () => {
+    this.whenSceneLoaded(() => {
+      this.cursorController.components["cursor-controller"].disable();
+    });
+  };
+
   enterScene = async (mediaStream, enterInVR) => {
     const playerCamera = document.querySelector("#player-camera");
     playerCamera.removeAttribute("scene-preview-camera");
@@ -68,23 +75,30 @@ export default class SceneEntryManager {
       return;
     }
 
+    this.scene.classList.remove("hand-cursor");
     this.scene.classList.add("no-cursor");
 
+    const cursor = this.cursorController.components["cursor-controller"];
+    cursor.enable();
+    cursor.setCursorVisibility(true);
+
     this.hubChannel.sendEntryEvent().then(() => {
       this.store.update({ activity: { lastEnteredAt: new Date().toISOString() } });
     });
   };
 
-  enterSceneWhenLoaded = (mediaStream, enterInVR) => {
-    const enterSceneImmediately = () => this.enterScene(mediaStream, enterInVR);
-
+  whenSceneLoaded = callback => {
     if (this.scene.hasLoaded) {
-      enterSceneImmediately();
+      callback();
     } else {
-      this.scene.addEventListener("loaded", enterSceneImmediately);
+      this.scene.addEventListener("loaded", callback);
     }
   };
 
+  enterSceneWhenLoaded = (mediaStream, enterInVR) => {
+    this.whenSceneLoaded(() => this.enterScene(mediaStream, enterInVR));
+  };
+
   exitScene = () => {
     if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
       NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());