fxaPairDevice.js (5044B)
1 // This Source Code Form is subject to the terms of the Mozilla Public 2 // License, v. 2.0. If a copy of the MPL was not distributed with this 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 const { XPCOMUtils } = ChromeUtils.importESModule( 6 "resource://gre/modules/XPCOMUtils.sys.mjs" 7 ); 8 const { FxAccounts } = ChromeUtils.importESModule( 9 "resource://gre/modules/FxAccounts.sys.mjs" 10 ); 11 const { Weave } = ChromeUtils.importESModule( 12 "resource://services-sync/main.sys.mjs" 13 ); 14 15 ChromeUtils.defineESModuleGetters(this, { 16 EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", 17 FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs", 18 QR: "moz-src:///toolkit/components/qrcode/encoder.mjs", 19 }); 20 21 // This is only for "labor illusion", see 22 // https://www.fastcompany.com/3061519/the-ux-secret-that-will-ruin-apps-for-you 23 const MIN_PAIRING_LOADING_TIME_MS = 1000; 24 25 /** 26 * Communication between FxAccountsPairingFlow and gFxaPairDeviceDialog 27 * is done using an emitter via the following messages: 28 * <- [view:SwitchToWebContent] - Notifies the view to navigate to a specific URL. 29 * <- [view:Error] - Notifies the view something went wrong during the pairing process. 30 * -> [view:Closed] - Notifies the pairing module the view was closed. 31 */ 32 var gFxaPairDeviceDialog = { 33 init() { 34 window.addEventListener("unload", () => this.uninit()); 35 document 36 .getElementById("qrError") 37 .addEventListener("click", () => this.startPairingFlow()); 38 39 this._resetBackgroundQR(); 40 // We let the modal show itself before eventually showing a primary-password dialog later. 41 Services.tm.dispatchToMainThread(() => this.startPairingFlow()); 42 }, 43 44 uninit() { 45 // When the modal closes we want to remove any query params 46 // To prevent refreshes/restores from reopening the dialog 47 const browser = window.docShell.chromeEventHandler; 48 browser.loadURI(Services.io.newURI("about:preferences#sync"), { 49 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 50 }); 51 52 this.teardownListeners(); 53 this._emitter.emit("view:Closed"); 54 }, 55 56 async startPairingFlow() { 57 this._resetBackgroundQR(); 58 document 59 .getElementById("qrWrapper") 60 .setAttribute("pairing-status", "loading"); 61 this._emitter = new EventEmitter(); 62 this.setupListeners(); 63 try { 64 if (!Weave.Utils.ensureMPUnlocked()) { 65 throw new Error("Master-password locked."); 66 } 67 // To keep consistent with our accounts.firefox.com counterpart 68 // we restyle the parent dialog this is contained in 69 this._styleParentDialog(); 70 71 const [, uri] = await Promise.all([ 72 new Promise(res => setTimeout(res, MIN_PAIRING_LOADING_TIME_MS)), 73 FxAccountsPairingFlow.start({ emitter: this._emitter }), 74 ]); 75 const imgData = QR.encodeToDataURI(uri, "L"); 76 document.getElementById("qrContainer").style.backgroundImage = 77 `url("${imgData.src}")`; 78 document 79 .getElementById("qrWrapper") 80 .setAttribute("pairing-status", "ready"); 81 } catch (e) { 82 this.onError(e); 83 } 84 }, 85 86 _styleParentDialog() { 87 // Since the dialog title is in the above document, we can't query the 88 // document in this level and need to go up one 89 let dialogParent = window.parent.document; 90 91 // To allow the firefox icon to go over the dialog 92 let dialogBox = dialogParent.querySelector(".dialogBox"); 93 dialogBox.style.overflow = "visible"; 94 dialogBox.style.borderRadius = "12px"; 95 96 let dialogTitle = dialogParent.querySelector(".dialogTitleBar"); 97 dialogTitle.style.borderBottom = "none"; 98 dialogTitle.classList.add("fxaPairDeviceIcon"); 99 }, 100 101 _resetBackgroundQR() { 102 // The text we encode doesn't really matter as it is un-scannable (blurry and very transparent). 103 const imgData = QR.encodeToDataURI( 104 "https://accounts.firefox.com/pair", 105 "L" 106 ); 107 document.getElementById("qrContainer").style.backgroundImage = 108 `url("${imgData.src}")`; 109 }, 110 111 onError(err) { 112 console.error(err); 113 this.teardownListeners(); 114 document 115 .getElementById("qrWrapper") 116 .setAttribute("pairing-status", "error"); 117 }, 118 119 _switchToUrl(url) { 120 const browser = window.docShell.chromeEventHandler; 121 browser.fixupAndLoadURIString(url, { 122 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 123 {} 124 ), 125 }); 126 }, 127 128 setupListeners() { 129 this._switchToWebContent = (_, url) => this._switchToUrl(url); 130 this._onError = (_, error) => this.onError(error); 131 this._emitter.once("view:SwitchToWebContent", this._switchToWebContent); 132 this._emitter.on("view:Error", this._onError); 133 }, 134 135 teardownListeners() { 136 try { 137 this._emitter.off("view:SwitchToWebContent", this._switchToWebContent); 138 this._emitter.off("view:Error", this._onError); 139 } catch (e) { 140 console.warn("Error while tearing down listeners.", e); 141 } 142 }, 143 }; 144 145 window.addEventListener("load", () => gFxaPairDeviceDialog.init());