diff --git a/src/assets/stylesheets/link-dialog.scss b/src/assets/stylesheets/link-dialog.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f9b18bf26832c614506f0681f1a52cc9fc79528b
--- /dev/null
+++ b/src/assets/stylesheets/link-dialog.scss
@@ -0,0 +1,27 @@
+:local(.domain) {
+  color: white;
+  font-size: 3.0em;
+  font-family: monospace;
+  font-weight: bold;
+  padding: 14px;
+}
+
+:local(.code) {
+  color: white;
+  font-size: 4.0em;
+  font-family: monospace;
+  font-weight: bold;
+  padding: 8px;
+}
+
+:local(.keep-open) {
+  font-size: 0.8em;
+}
+
+:local(.digit) {
+  padding: 0px 8px;
+}
+
+:local(.code-loading-panel) {
+  background: transparent;
+}
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 27c3ffefade88834db8df5692a8316217ae21264..02a9a318a4bcd66990205dd92b145813ed7ae4ad 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -60,6 +60,10 @@
     "home.environment_author_by": " by ",
     "home.dialog.close": "CLOSE",
     "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in",
-    "mailing_list.privacy_link": "this Privacy Notice"
+    "mailing_list.privacy_link": "this Privacy Notice",
+    "link.in_your_browser": "In your device's browser, go to:",
+    "link.link_domain": "hub.link",
+    "link.enter_code": "Then, enter code:",
+    "link.do_not_close": "Keep this dialog open to use this code."
   }
 }
diff --git a/src/hub.js b/src/hub.js
index 2d3ea5d9cee82852d2fa4621541c5d5f1571e130..6f36492b1ade030641938c9cee7fceaf2cdc4718 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -172,7 +172,7 @@ const onReady = async () => {
 
   registerNetworkSchemas();
 
-  let uiProps = {};
+  let uiProps = { linkChannel };
 
   mountUI(scene);
 
diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js
index a502fe83affefab13912d59220704bf47016a9b5..41eab23e95492cc3a298f75a27768094fd9fdac8 100644
--- a/src/react-components/info-dialog.js
+++ b/src/react-components/info-dialog.js
@@ -22,8 +22,7 @@ class InfoDialog extends Component {
     dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)),
     onCloseDialog: PropTypes.func,
     onSubmittedEmail: PropTypes.func,
-    linkChannel: PropTypes.object,
-    onLinkCodeUsed: PropTypes.func
+    linkCode: PropTypes.string
   };
 
   constructor(props) {
@@ -219,8 +218,8 @@ class InfoDialog extends Component {
         );
         break;
       case InfoDialog.dialogTypes.link:
-        dialogTitle = "Enter on Device";
-        dialogBody = <LinkDialog linkChannel={this.props.linkChannel} onLinkCodeUsed={this.props.onLinkCodeUseds} />;
+        dialogTitle = "Link to Device";
+        dialogBody = <LinkDialog linkCode={this.props.linkCode} />;
         break;
     }
 
diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js
index e12aa237a6363e8eeb9cebb97b6d76d1c5fc7660..c6d8305f8039c43679b9c4ae57113967e2c377bb 100644
--- a/src/react-components/link-dialog.js
+++ b/src/react-components/link-dialog.js
@@ -1,20 +1,53 @@
 import React, { Component } from "react";
-import classNames from "classnames";
 import PropTypes from "prop-types";
+import classNames from "classnames";
 import { FormattedMessage } from "react-intl";
 
-class LinkDialog extends Component {
-  state = {
-    code: null
-  };
+import styles from "../assets/stylesheets/link-dialog.scss";
 
+class LinkDialog extends Component {
   static propTypes = {
-    linkChannel: PropTypes.object,
-    onLinkCodeUsed: PropTypes.func
+    linkCode: PropTypes.string
   };
 
   render() {
-    return <div>Hello</div>;
+    if (!this.props.linkCode) {
+      return (
+        <div>
+          <div className={classNames("loading-panel", styles.codeLoadingPanel)}>
+            <div className="loader-wrap">
+              <div className="loader">
+                <div className="loader-center" />
+              </div>
+            </div>
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <div>
+        <div>
+          <FormattedMessage id="link.in_your_browser" />
+        </div>
+        <div className={styles.domain}>
+          <FormattedMessage id="link.link_domain" />
+        </div>
+        <div>
+          <FormattedMessage id="link.enter_code" />
+        </div>
+        <div className={styles.code}>
+          {this.props.linkCode.split("").map((d, i) => (
+            <span className={styles.digit} key={i}>
+              {d}
+            </span>
+          ))}
+        </div>
+        <div className={styles.keepOpen}>
+          <FormattedMessage id="link.do_not_close" />
+        </div>
+      </div>
+    );
   }
 }
 
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 8ec0f4a99e2c57225578b8572182096f4cf5cc8d..b390773ed0b0aaf56aa50a124fbfd8398283c5b8 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -66,6 +66,7 @@ class UIRoot extends Component {
     enableScreenSharing: PropTypes.bool,
     store: PropTypes.object,
     scene: PropTypes.object,
+    linkChannel: PropTypes.object,
     htmlPrefix: PropTypes.string,
     showProfileEntry: PropTypes.bool,
     availableVREntryTypes: PropTypes.object,
@@ -81,6 +82,8 @@ class UIRoot extends Component {
     entryStep: ENTRY_STEPS.start,
     enterInVR: false,
     infoDialogType: null,
+    linkCode: null,
+    linkCodeCancel: null,
 
     shareScreen: false,
     requestedScreen: false,
@@ -519,6 +522,21 @@ class UIRoot extends Component {
     this.setState({ entryStep: ENTRY_STEPS.finished });
   };
 
+  attemptLink = async () => {
+    this.setState({ infoDialogType: InfoDialog.dialogTypes.link });
+    const { code, cancel, onFinished } = await this.props.linkChannel.generateCode();
+    this.setState({ linkCode: code, linkCodeCancel: cancel });
+    onFinished.then(this.handleCloseDialog);
+  };
+
+  handleCloseDialog = async () => {
+    if (this.state.linkCodeCancel) {
+      this.state.linkCodeCancel();
+    }
+
+    this.setState({ infoDialogType: null, linkCode: null, linkCodeCancel: null });
+  };
+
   render() {
     if (this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason) {
       let subtitle = null;
@@ -618,7 +636,7 @@ class UIRoot extends Component {
             {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
               <GenericEntryButton onClick={this.enterVR} />
             )}
-            <DeviceEntryButton onClick={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.link })} />
+            <DeviceEntryButton onClick={this.attemptLink} />
             {this.props.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && (
               <GearVREntryButton onClick={this.enterGearVR} />
             )}
@@ -836,8 +854,9 @@ class UIRoot extends Component {
         <div className="ui">
           <InfoDialog
             dialogType={this.state.infoDialogType}
+            linkCode={this.state.linkCode}
             onSubmittedEmail={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.email_submitted })}
-            onCloseDialog={() => this.setState({ infoDialogType: null })}
+            onCloseDialog={this.handleCloseDialog}
           />
 
           {this.state.entryStep === ENTRY_STEPS.finished && (
diff --git a/src/utils/link-channel.js b/src/utils/link-channel.js
index 418b6d73800289f946624dd61e6b08a83d31d7d8..4f61fd3aa41281d2481a1d4d30c858fa927540b1 100644
--- a/src/utils/link-channel.js
+++ b/src/utils/link-channel.js
@@ -28,9 +28,16 @@ export default class LinkChannel {
 
           // Only respond to one link_request in this channel.
           let readyToSend = false;
+          let leftChannel = false;
 
           const channel = this.socket.channel(`link:${code}`, { timeout: 10000 });
-          const cancel = () => channel.leave();
+
+          const leave = () => {
+            if (!leftChannel) channel.leave();
+            leftChannel = true;
+          };
+
+          const cancel = () => leave();
 
           channel.on("link_expired", () => finished("expired"));
 
@@ -63,8 +70,12 @@ export default class LinkChannel {
                     data: encryptedData
                   };
 
-                  channel.push("link_response", payload);
-                  channel.leave();
+                  if (!leftChannel) {
+                    channel.push("link_response", payload);
+                  }
+
+                  leave();
+
                   finished("used");
                   readyToSend = false;
                 }