summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/fxaPairDevice.js
blob: 8f5c26734db5132adf33657dae2eed2b5e51184b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// 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);
    }
  },
};