diff --git a/.eslintignore b/.eslintignore index dd0755fe7b68..23d2863f0b04 100644 --- a/.eslintignore +++ b/.eslintignore @@ -294,6 +294,9 @@ security/nss/** # services/ exclusions +# Webpack-bundled library +services/fxaccounts/FxAccountsPairingChannel.js + # Uses `#filter substitution` services/sync/modules/constants.js services/sync/services-sync.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 69030f7c2388..976393e43074 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1416,6 +1416,12 @@ pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox // The remote URL of the FxA OAuth Server pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1"); +// Whether FxA pairing using QR codes is enabled. +pref("identity.fxaccounts.pairing.enabled", false); + +// The remote URI of the FxA pairing server +pref("identity.fxaccounts.remote.pairing.uri", "wss://channelserver.services.mozilla.com"); + // Token server used by the FxA Sync identity. pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5"); diff --git a/browser/components/preferences/in-content/fxaPairDevice.js b/browser/components/preferences/in-content/fxaPairDevice.js new file mode 100644 index 000000000000..0046b165ef95 --- /dev/null +++ b/browser/components/preferences/in-content/fxaPairDevice.js @@ -0,0 +1,98 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {FxAccounts} = ChromeUtils.import("resource://gre/modules/FxAccounts.jsm"); +const {Weave} = ChromeUtils.import("resource://services-sync/main.js"); + +XPCOMUtils.defineLazyModuleGetters(this, { + EventEmitter: "resource://gre/modules/EventEmitter.jsm", + FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.jsm", +}); +const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {}); +const QR = require("devtools/shared/qrcode/index"); + +// This is only for "labor illusion", see +// https://www.fastcompany.com/3061519/the-ux-secret-that-will-ruin-apps-for-you +const MIN_PAIRING_LOADING_TIME_MS = 1000; + +/** + * Communication between FxAccountsPairingFlow and gFxaPairDeviceDialog + * is done using an emitter via the following messages: + * <- [view:SwitchToWebContent] - Notifies the view to navigate to a specific URL. + * <- [view:Error] - Notifies the view something went wrong during the pairing process. + * -> [view:Closed] - Notifies the pairing module the view was closed. + */ +var gFxaPairDeviceDialog = { + init() { + this._resetBackgroundQR(); + FxAccounts.config.promiseConnectDeviceURI("pairing-modal").then(connectURI => { + document.getElementById("connect-another-device-link").setAttribute("href", connectURI); + }); + // We let the modal show itself before eventually showing a master-password dialog later. + Services.tm.dispatchToMainThread(() => this.startPairingFlow()); + }, + + uninit() { + this.teardownListeners(); + this._emitter.emit("view:Closed"); + }, + + async startPairingFlow() { + this._resetBackgroundQR(); + document.getElementById("qrWrapper").setAttribute("pairing-status", "loading"); + this._emitter = new EventEmitter(); + this.setupListeners(); + try { + if (!Weave.Utils.ensureMPUnlocked()) { + throw new Error("Master-password locked."); + } + const [, uri] = await Promise.all([ + new Promise(res => setTimeout(res, MIN_PAIRING_LOADING_TIME_MS)), + FxAccountsPairingFlow.start({emitter: this._emitter}), + ]); + const imgData = QR.encodeToDataURI(uri, "L"); + document.getElementById("qrContainer").style.backgroundImage = `url("${imgData.src}")`; + document.getElementById("qrWrapper").setAttribute("pairing-status", "ready"); + } catch (e) { + this.onError(e); + } + }, + + _resetBackgroundQR() { + // The text we encode doesn't really matter as it is un-scannable (blurry and very transparent). + const imgData = QR.encodeToDataURI("https://accounts.firefox.com/pair", "L"); + document.getElementById("qrContainer").style.backgroundImage = `url("${imgData.src}")`; + }, + + onError(err) { + Cu.reportError(err); + this.teardownListeners(); + document.getElementById("qrWrapper").setAttribute("pairing-status", "error"); + }, + + _switchToUrl(url) { + const browser = window.docShell.chromeEventHandler; + browser.loadURI(url, { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}), + }); + }, + + setupListeners() { + this._switchToWebContent = (_, url) => this._switchToUrl(url); + this._onError = (_, error) => this.onError(error); + this._emitter.once("view:SwitchToWebContent", this._switchToWebContent); + this._emitter.on("view:Error", this._onError); + }, + + teardownListeners() { + try { + this._emitter.off("view:SwitchToWebContent", this._switchToWebContent); + this._emitter.off("view:Error", this._onError); + } catch (e) { + console.warn("Error while tearing down listeners.", e); + } + }, +}; diff --git a/browser/components/preferences/in-content/fxaPairDevice.xul b/browser/components/preferences/in-content/fxaPairDevice.xul new file mode 100644 index 000000000000..e8be4a73b629 --- /dev/null +++ b/browser/components/preferences/in-content/fxaPairDevice.xul @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + +