"weave:service:sync:start": this.toggleSyncActivity(true); break; case "weave:service:sync:finish": case "weave:service:sync:error": this.toggleSyncActivity(false); break; default: this.refreshState().catch(e => { console.error(e); }); break; } }, // Builds a new state from scratch. async refreshState() { const newState = {}; await this._refreshFxAState(newState); // Optimize the "not signed in" case to avoid refreshing twice just after // startup - if there's currently no _state, and we still aren't configured, // just early exit. if (this._state == null && newState.status == DEFAULT_STATE.status) { return this.state; } if (newState.syncEnabled) { this._setLastSyncTime(newState); // We want this in case we change accounts. } this._state = newState; this.notifyStateUpdated(); return this.state; }, // Update the current state with the last sync time/currently syncing status. toggleSyncActivity(syncing) { this._syncing = syncing; this._setLastSyncTime(this._state); this.notifyStateUpdated(); }, notifyStateUpdated() { Services.obs.notifyObservers(null, ON_UPDATE); }, async _refreshFxAState(newState) { let userData = await this._getUserData(); await this._populateWithUserData(newState, userData); }, async _populateWithUserData(state, userData) { let status; let syncUserName = Services.prefs.getStringPref( "services.sync.username", "" ); if (!userData) { // If Sync thinks it is configured but there's no FxA user, then we // want to enter the "login failed" state so the user can get // reconfigured. if (syncUserName) { state.email = syncUserName; status = STATUS_LOGIN_FAILED; } else { // everyone agrees nothing is configured. status = STATUS_NOT_CONFIGURED; } } else { let loginFailed = await this._loginFailed(); if (loginFailed) { status = STATUS_LOGIN_FAILED; } else if (!userData.verified) { status = STATUS_NOT_VERIFIED; } else { status = STATUS_SIGNED_IN; } state.uid = userData.uid; state.email = userData.email; state.displayName = userData.displayName; // for better or worse, this module renames these attribues. state.avatarURL = userData.avatar; state.avatarIsDefault = userData.avatarDefault; state.syncEnabled = !!syncUserName; state.hasSyncKeys = await this.fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC); } state.status = status; }, async _getUserData() { try { return await this.fxAccounts.getSignedInUser(); } catch (e) { // This is most likely in tests, where we quickly log users in and out. // The most likely scenario is a user logged out, so reflect that. // Bug 995134 calls for better errors so we could retry if we were // sure this was the failure reason. console.error("Error updating FxA account info:", e); return null; } }, _setLastSyncTime(state) { if (state?.status == UIState.STATUS_SIGNED_IN) { const lastSync = Services.prefs.getStringPref( "services.sync.lastSync", null ); state.lastSync = lastSync ? new Date(lastSync) : null; } }, async _loginFailed() { // First ask FxA if it thinks the user needs re-authentication. In practice, // this check is probably canonical (ie, we probably don't really need // the check below at all as we drop local session info on the first sign // of a problem) - but we keep it for now to keep the risk down. let hasLocalSession = await this.fxAccounts.hasLocalSession(); if (!hasLocalSession) { return true; } // Referencing Weave.Service will implicitly initialize sync, and we don't // want to force that - so first check if it is ready. let service = Cc["@mozilla.org/weave/service;1"].getService( Ci.nsISupports ).wrappedJSObject; if (!service.ready) { return false; } // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in". // All other login failures are assumed to be transient and should go // away by themselves, so aren't reflected here. return lazy.Weave.Status.login == lazy.LOGIN_FAILED_LOGIN_REJECTED; }, set fxAccounts(mockFxAccounts) { delete this.fxAccounts; this.fxAccounts = mockFxAccounts; }, }; ChromeUtils.defineLazyGetter(UIStateInternal, "fxAccounts", () => { return ChromeUtils.importESModule( "resource://gre/modules/FxAccounts.sys.mjs" ).getFxAccountsSingleton(); }); for (let topic of TOPICS) { Services.obs.addObserver(UIStateInternal, topic); } export var UIState = { _internal: UIStateInternal, ON_UPDATE, STATUS_NOT_CONFIGURED, STATUS_LOGIN_FAILED, STATUS_NOT_VERIFIED, STATUS_SIGNED_IN, /** * Returns true if the module has been initialized and the state set. * If not, return false and trigger an init in the background. */ isReady() { return this._internal.isReady(); }, /** * @returns {UIState} The current Sync/FxA UI State. */ get() { return this._internal.state; }, /** * Refresh the state. Used for testing, don't call this directly since * UIState already listens to Sync/FxA notifications to determine if the state * needs to be refreshed. ON_UPDATE will be fired once the state is refreshed. * * @returns {Promise} Resolved once the state is refreshed. */ refresh() { return this._internal.refreshState(); }, /** * Reset the state of the whole module. Used for testing. */ reset() { this._internal.reset(); }, }; PK