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"use strict";
6
7/**
8 * @typedef {Object} UIState
9 * @property {string} status The Sync/FxA status, see STATUS_* constants.
10 * @property {string} [email] The FxA email configured to log-in with Sync.
11 * @property {string} [displayName] The user's FxA display name.
12 * @property {string} [avatarURL] The user's FxA avatar URL.
13 * @property {Date} [lastSync] The last sync time.
14 * @property {boolean} [syncing] Whether or not we are currently syncing.
15 */
16
17var EXPORTED_SYMBOLS = ["UIState"];
18
19const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
20ChromeUtils.defineModuleGetter(
21  this,
22  "Weave",
23  "resource://services-sync/main.js"
24);
25
26const TOPICS = [
27  "weave:connected",
28  "weave:service:login:got-hashed-id",
29  "weave:service:login:error",
30  "weave:service:ready",
31  "weave:service:sync:start",
32  "weave:service:sync:finish",
33  "weave:service:sync:error",
34  "weave:service:start-over:finish",
35  "fxaccounts:onverified",
36  "fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
37  "fxaccounts:onlogout",
38  "fxaccounts:profilechange",
39  "fxaccounts:statechange",
40];
41
42const ON_UPDATE = "sync-ui-state:update";
43
44const STATUS_NOT_CONFIGURED = "not_configured";
45const STATUS_LOGIN_FAILED = "login_failed";
46const STATUS_NOT_VERIFIED = "not_verified";
47const STATUS_SIGNED_IN = "signed_in";
48
49const DEFAULT_STATE = {
50  status: STATUS_NOT_CONFIGURED,
51};
52
53const UIStateInternal = {
54  _initialized: false,
55  _state: null,
56
57  // We keep _syncing out of the state object because we can only track it
58  // using sync events and we can't determine it at any point in time.
59  _syncing: false,
60
61  get state() {
62    if (!this._state) {
63      return DEFAULT_STATE;
64    }
65    return Object.assign({}, this._state, { syncing: this._syncing });
66  },
67
68  isReady() {
69    if (!this._initialized) {
70      this.init();
71      return false;
72    }
73    return true;
74  },
75
76  init() {
77    this._initialized = true;
78    // Because the FxA toolbar is usually visible, this module gets loaded at
79    // browser startup, and we want to avoid pulling in all of FxA or Sync at
80    // that time, so we refresh the state after the browser has settled.
81    Services.tm.idleDispatchToMainThread(() => {
82      this.refreshState().catch(e => {
83        Cu.reportError(e);
84      });
85    }, 2000);
86  },
87
88  // Used for testing.
89  reset() {
90    this._state = null;
91    this._syncing = false;
92    this._initialized = false;
93  },
94
95  observe(subject, topic, data) {
96    switch (topic) {
97      case "weave:service:sync:start":
98        this.toggleSyncActivity(true);
99        break;
100      case "weave:service:sync:finish":
101      case "weave:service:sync:error":
102        this.toggleSyncActivity(false);
103        break;
104      default:
105        this.refreshState().catch(e => {
106          Cu.reportError(e);
107        });
108        break;
109    }
110  },
111
112  // Builds a new state from scratch.
113  async refreshState() {
114    const newState = {};
115    await this._refreshFxAState(newState);
116    // Optimize the "not signed in" case to avoid refreshing twice just after
117    // startup - if there's currently no _state, and we still aren't configured,
118    // just early exit.
119    if (this._state == null && newState.status == DEFAULT_STATE.status) {
120      return this.state;
121    }
122    if (newState.syncEnabled) {
123      this._setLastSyncTime(newState); // We want this in case we change accounts.
124    }
125    this._state = newState;
126
127    this.notifyStateUpdated();
128    return this.state;
129  },
130
131  // Update the current state with the last sync time/currently syncing status.
132  toggleSyncActivity(syncing) {
133    this._syncing = syncing;
134    this._setLastSyncTime(this._state);
135
136    this.notifyStateUpdated();
137  },
138
139  notifyStateUpdated() {
140    Services.obs.notifyObservers(null, ON_UPDATE);
141  },
142
143  async _refreshFxAState(newState) {
144    let userData = await this._getUserData();
145    await this._populateWithUserData(newState, userData);
146  },
147
148  async _populateWithUserData(state, userData) {
149    let status;
150    let syncUserName = Services.prefs.getStringPref(
151      "services.sync.username",
152      ""
153    );
154    if (!userData) {
155      // If Sync thinks it is configured but there's no FxA user, then we
156      // want to enter the "login failed" state so the user can get
157      // reconfigured.
158      if (syncUserName) {
159        state.email = syncUserName;
160        status = STATUS_LOGIN_FAILED;
161      } else {
162        // everyone agrees nothing is configured.
163        status = STATUS_NOT_CONFIGURED;
164      }
165    } else {
166      let loginFailed = await this._loginFailed();
167      if (loginFailed) {
168        status = STATUS_LOGIN_FAILED;
169      } else if (!userData.verified) {
170        status = STATUS_NOT_VERIFIED;
171      } else {
172        status = STATUS_SIGNED_IN;
173      }
174      state.uid = userData.uid;
175      state.email = userData.email;
176      state.displayName = userData.displayName;
177      // for better or worse, this module renames these attribues.
178      state.avatarURL = userData.avatar;
179      state.avatarIsDefault = userData.avatarDefault;
180      state.syncEnabled = !!syncUserName;
181    }
182    state.status = status;
183  },
184
185  async _getUserData() {
186    try {
187      return await this.fxAccounts.getSignedInUser();
188    } catch (e) {
189      // This is most likely in tests, where we quickly log users in and out.
190      // The most likely scenario is a user logged out, so reflect that.
191      // Bug 995134 calls for better errors so we could retry if we were
192      // sure this was the failure reason.
193      Cu.reportError("Error updating FxA account info: " + e);
194      return null;
195    }
196  },
197
198  _setLastSyncTime(state) {
199    if (state.status == UIState.STATUS_SIGNED_IN) {
200      const lastSync = Services.prefs.getCharPref(
201        "services.sync.lastSync",
202        null
203      );
204      state.lastSync = lastSync ? new Date(lastSync) : null;
205    }
206  },
207
208  async _loginFailed() {
209    // First ask FxA if it thinks the user needs re-authentication. In practice,
210    // this check is probably canonical (ie, we probably don't really need
211    // the check below at all as we drop local session info on the first sign
212    // of a problem) - but we keep it for now to keep the risk down.
213    let hasLocalSession = await this.fxAccounts.hasLocalSession();
214    if (!hasLocalSession) {
215      return true;
216    }
217
218    // Referencing Weave.Service will implicitly initialize sync, and we don't
219    // want to force that - so first check if it is ready.
220    let service = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
221      .wrappedJSObject;
222    if (!service.ready) {
223      return false;
224    }
225    // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
226    // All other login failures are assumed to be transient and should go
227    // away by themselves, so aren't reflected here.
228    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
229  },
230
231  set fxAccounts(mockFxAccounts) {
232    delete this.fxAccounts;
233    this.fxAccounts = mockFxAccounts;
234  },
235};
236
237ChromeUtils.defineModuleGetter(
238  UIStateInternal,
239  "fxAccounts",
240  "resource://gre/modules/FxAccounts.jsm"
241);
242
243for (let topic of TOPICS) {
244  Services.obs.addObserver(UIStateInternal, topic);
245}
246
247var UIState = {
248  _internal: UIStateInternal,
249
250  ON_UPDATE,
251
252  STATUS_NOT_CONFIGURED,
253  STATUS_LOGIN_FAILED,
254  STATUS_NOT_VERIFIED,
255  STATUS_SIGNED_IN,
256
257  /**
258   * Returns true if the module has been initialized and the state set.
259   * If not, return false and trigger an init in the background.
260   */
261  isReady() {
262    return this._internal.isReady();
263  },
264
265  /**
266   * @returns {UIState} The current Sync/FxA UI State.
267   */
268  get() {
269    return this._internal.state;
270  },
271
272  /**
273   * Refresh the state. Used for testing, don't call this directly since
274   * UIState already listens to Sync/FxA notifications to determine if the state
275   * needs to be refreshed. ON_UPDATE will be fired once the state is refreshed.
276   *
277   * @returns {Promise<UIState>} Resolved once the state is refreshed.
278   */
279  refresh() {
280    return this._internal.refreshState();
281  },
282
283  /**
284   * Reset the state of the whole module. Used for testing.
285   */
286  reset() {
287    this._internal.reset();
288  },
289};
290