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