// 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.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const { FxAccounts } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"
);
const { Weave } = ChromeUtils.importESModule(
"resource://services-sync/main.sys.mjs"
);
ChromeUtils.defineESModuleGetters(
this, {
EventEmitter:
"resource://gre/modules/EventEmitter.sys.mjs",
FxAccountsPairingFlow:
"resource://gre/modules/FxAccountsPairing.sys.mjs",
});
const { require } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/Loader.sys.mjs"
);
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();
// We let the modal show itself before eventually showing a primary-password dialog later.
Services.tm.dispatchToMainThread(() =>
this.startPairingFlow());
},
uninit() {
// When the modal closes we want to remove any query params
// To prevent refreshes/restores from reopening the dialog
const browser = window.docShell.chromeEventHandler;
browser.loadURI(Services.io.newURI(
"about:preferences#sync"), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
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.");
}
// To keep consistent with our accounts.firefox.com counterpart
// we restyle the parent dialog this is contained in
this._styleParentDialog();
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);
}
},
_styleParentDialog() {
// Since the dialog title is in the above document, we can't query the
// document in this level and need to go up one
let dialogParent = window.parent.document;
// To allow the firefox icon to go over the dialog
let dialogBox = dialogParent.querySelector(
".dialogBox");
dialogBox.style.overflow =
"visible";
dialogBox.style.borderRadius =
"12px";
let dialogTitle = dialogParent.querySelector(
".dialogTitleBar");
dialogTitle.style.borderBottom =
"none";
dialogTitle.classList.add(
"fxaPairDeviceIcon");
},
_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) {
console.error(err);
this.teardownListeners();
document
.getElementById(
"qrWrapper")
.setAttribute(
"pairing-status",
"error");
},
_switchToUrl(url) {
const browser = window.docShell.chromeEventHandler;
browser.fixupAndLoadURIString(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);
}
},
};