forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			440 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			440 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| /**
 | |
|  * This module exports the TabsSetupFlowManager singleton, which manages the state and
 | |
|  * diverse inputs which drive the Firefox View synced tabs setup flow
 | |
|  */
 | |
| 
 | |
| const { XPCOMUtils } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/XPCOMUtils.sys.mjs"
 | |
| );
 | |
| 
 | |
| const lazy = {};
 | |
| XPCOMUtils.defineLazyModuleGetters(lazy, {
 | |
|   Log: "resource://gre/modules/Log.jsm",
 | |
|   UIState: "resource://services-sync/UIState.jsm",
 | |
|   SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
 | |
|   return ChromeUtils.import(
 | |
|     "resource://gre/modules/FxAccounts.jsm"
 | |
|   ).getFxAccountsSingleton();
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   lazy,
 | |
|   "gNetworkLinkService",
 | |
|   "@mozilla.org/network/network-link-service;1",
 | |
|   "nsINetworkLinkService"
 | |
| );
 | |
| 
 | |
| const SYNC_TABS_PREF = "services.sync.engine.tabs";
 | |
| const RECENT_TABS_SYNC = "services.sync.lastTabFetch";
 | |
| const MOBILE_PROMO_DISMISSED_PREF =
 | |
|   "browser.tabs.firefox-view.mobilePromo.dismissed";
 | |
| const LOGGING_PREF = "browser.tabs.firefox-view.logLevel";
 | |
| const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
 | |
| const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
 | |
| const NETWORK_STATUS_CHANGED = "network:offline-status-changed";
 | |
| const SYNC_SERVICE_ERROR = "weave:service:sync:error";
 | |
| const FXA_ENABLED = "identity.fxaccounts.enabled";
 | |
| const SYNC_SERVICE_FINISHED = "weave:service:sync:finish";
 | |
| 
 | |
| function openTabInWindow(window, url) {
 | |
|   const {
 | |
|     switchToTabHavingURI,
 | |
|   } = window.docShell.chromeEventHandler.ownerGlobal;
 | |
|   switchToTabHavingURI(url, true, {});
 | |
| }
 | |
| 
 | |
| export const TabsSetupFlowManager = new (class {
 | |
|   constructor() {
 | |
|     this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
 | |
| 
 | |
|     this.setupState = new Map();
 | |
|     this.resetInternalState();
 | |
|     this._currentSetupStateName = "";
 | |
|     this.networkIsOnline =
 | |
|       lazy.gNetworkLinkService.linkStatusKnown &&
 | |
|       lazy.gNetworkLinkService.isLinkUp;
 | |
|     this.syncIsWorking = true;
 | |
|     this.syncIsConnected = lazy.UIState.get().syncEnabled;
 | |
| 
 | |
|     this.registerSetupState({
 | |
|       uiStateIndex: 0,
 | |
|       name: "error-state",
 | |
|       exitConditions: () => {
 | |
|         return (
 | |
|           this.networkIsOnline &&
 | |
|           (this.syncIsWorking || this.syncHasWorked) &&
 | |
|           !Services.prefs.prefIsLocked(FXA_ENABLED) &&
 | |
|           // its only an error for sync to not be connected if we are signed-in.
 | |
|           (this.syncIsConnected || !this.fxaSignedIn)
 | |
|         );
 | |
|       },
 | |
|     });
 | |
|     this.registerSetupState({
 | |
|       uiStateIndex: 1,
 | |
|       name: "not-signed-in",
 | |
|       exitConditions: () => {
 | |
|         return this.fxaSignedIn;
 | |
|       },
 | |
|     });
 | |
|     this.registerSetupState({
 | |
|       uiStateIndex: 2,
 | |
|       name: "connect-secondary-device",
 | |
|       exitConditions: () => {
 | |
|         return this.secondaryDeviceConnected;
 | |
|       },
 | |
|     });
 | |
|     this.registerSetupState({
 | |
|       uiStateIndex: 3,
 | |
|       name: "disabled-tab-sync",
 | |
|       exitConditions: () => {
 | |
|         return this.syncTabsPrefEnabled;
 | |
|       },
 | |
|     });
 | |
|     this.registerSetupState({
 | |
|       uiStateIndex: 4,
 | |
|       name: "synced-tabs-not-ready",
 | |
|       enter: () => {
 | |
|         if (!this.didRecentTabSync) {
 | |
|           lazy.SyncedTabs.syncTabs();
 | |
|         }
 | |
|       },
 | |
|       exitConditions: () => {
 | |
|         return this.didRecentTabSync;
 | |
|       },
 | |
|     });
 | |
|     this.registerSetupState({
 | |
|       uiStateIndex: 5,
 | |
|       name: "synced-tabs-loaded",
 | |
|       exitConditions: () => {
 | |
|         // This is the end state
 | |
|         return false;
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     Services.obs.addObserver(this, lazy.UIState.ON_UPDATE);
 | |
|     Services.obs.addObserver(this, TOPIC_DEVICELIST_UPDATED);
 | |
|     Services.obs.addObserver(this, NETWORK_STATUS_CHANGED);
 | |
|     Services.obs.addObserver(this, SYNC_SERVICE_ERROR);
 | |
|     Services.obs.addObserver(this, SYNC_SERVICE_FINISHED);
 | |
| 
 | |
|     // this.syncTabsPrefEnabled will track the value of the tabs pref
 | |
|     XPCOMUtils.defineLazyPreferenceGetter(
 | |
|       this,
 | |
|       "syncTabsPrefEnabled",
 | |
|       SYNC_TABS_PREF,
 | |
|       false,
 | |
|       () => {
 | |
|         this._uiUpdateNeeded = true;
 | |
|         this.maybeUpdateUI();
 | |
|       }
 | |
|     );
 | |
|     XPCOMUtils.defineLazyPreferenceGetter(
 | |
|       this,
 | |
|       "lastTabFetch",
 | |
|       RECENT_TABS_SYNC,
 | |
|       0,
 | |
|       () => {
 | |
|         this._uiUpdateNeeded = true;
 | |
|         this.maybeUpdateUI();
 | |
|       }
 | |
|     );
 | |
|     XPCOMUtils.defineLazyPreferenceGetter(
 | |
|       this,
 | |
|       "mobilePromoDismissedPref",
 | |
|       MOBILE_PROMO_DISMISSED_PREF,
 | |
|       false,
 | |
|       () => {
 | |
|         this._uiUpdateNeeded = true;
 | |
|         this.maybeUpdateUI();
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     this._uiUpdateNeeded = true;
 | |
|     if (this.fxaSignedIn) {
 | |
|       this.refreshDevices();
 | |
|     }
 | |
|     this.maybeUpdateUI();
 | |
|   }
 | |
| 
 | |
|   resetInternalState() {
 | |
|     // assign initial values for all the managed internal properties
 | |
|     this._currentSetupStateName = "not-signed-in";
 | |
|     this._shouldShowSuccessConfirmation = false;
 | |
|     this._didShowMobilePromo = false;
 | |
|     this._uiUpdateNeeded = true;
 | |
| 
 | |
|     this.syncHasWorked = false;
 | |
| 
 | |
|     // keep track of what is connected so we can respond to changes
 | |
|     this._deviceStateSnapshot = {
 | |
|       mobileDeviceConnected: this.mobileDeviceConnected,
 | |
|       secondaryDeviceConnected: this.secondaryDeviceConnected,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   getErrorType() {
 | |
|     // this ordering is important for dealing with multiple errors at once
 | |
|     const errorStates = {
 | |
|       "network-offline": !this.networkIsOnline,
 | |
|       "sync-error": !this.syncIsWorking && !this.syncHasWorked,
 | |
|       "fxa-admin-disabled": Services.prefs.prefIsLocked(FXA_ENABLED),
 | |
|       "sync-disconnected": !this.syncIsConnected,
 | |
|     };
 | |
| 
 | |
|     for (let [type, value] of Object.entries(errorStates)) {
 | |
|       if (value) {
 | |
|         return type;
 | |
|       }
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   uninit() {
 | |
|     Services.obs.removeObserver(this, lazy.UIState.ON_UPDATE);
 | |
|     Services.obs.removeObserver(this, TOPIC_DEVICELIST_UPDATED);
 | |
|     Services.obs.removeObserver(this, NETWORK_STATUS_CHANGED);
 | |
|     Services.obs.removeObserver(this, SYNC_SERVICE_ERROR);
 | |
|     Services.obs.removeObserver(this, SYNC_SERVICE_FINISHED);
 | |
|   }
 | |
|   get currentSetupState() {
 | |
|     return this.setupState.get(this._currentSetupStateName);
 | |
|   }
 | |
|   get uiStateIndex() {
 | |
|     return this.currentSetupState.uiStateIndex;
 | |
|   }
 | |
|   get fxaSignedIn() {
 | |
|     let { UIState } = lazy;
 | |
|     return (
 | |
|       UIState.isReady() && UIState.get().status === UIState.STATUS_SIGNED_IN
 | |
|     );
 | |
|   }
 | |
|   get secondaryDeviceConnected() {
 | |
|     if (!this.fxaSignedIn) {
 | |
|       return false;
 | |
|     }
 | |
|     let recentDevices = lazy.fxAccounts.device?.recentDeviceList?.length;
 | |
|     return recentDevices > 1;
 | |
|   }
 | |
|   get didRecentTabSync() {
 | |
|     const nowSeconds = Math.floor(Date.now() / 1000);
 | |
|     return (
 | |
|       nowSeconds - this.lastTabFetch <
 | |
|       lazy.SyncedTabs.TABS_FRESH_ENOUGH_INTERVAL_SECONDS
 | |
|     );
 | |
|   }
 | |
|   get mobileDeviceConnected() {
 | |
|     if (!this.fxaSignedIn) {
 | |
|       return false;
 | |
|     }
 | |
|     let mobileClients = lazy.fxAccounts.device.recentDeviceList?.filter(
 | |
|       device => device.type == "mobile"
 | |
|     );
 | |
|     return mobileClients?.length > 0;
 | |
|   }
 | |
|   get shouldShowMobilePromo() {
 | |
|     return (
 | |
|       this.syncIsConnected &&
 | |
|       this.fxaSignedIn &&
 | |
|       this.currentSetupState.uiStateIndex >= 4 &&
 | |
|       !this.mobileDeviceConnected &&
 | |
|       !this.mobilePromoDismissedPref
 | |
|     );
 | |
|   }
 | |
|   get shouldShowMobileConnectedSuccess() {
 | |
|     return (
 | |
|       this.currentSetupState.uiStateIndex >= 3 &&
 | |
|       this._shouldShowSuccessConfirmation &&
 | |
|       this.mobileDeviceConnected
 | |
|     );
 | |
|   }
 | |
|   get logger() {
 | |
|     if (!this._log) {
 | |
|       let setupLog = lazy.Log.repository.getLogger("FirefoxView.TabsSetup");
 | |
|       setupLog.manageLevelFromPref(LOGGING_PREF);
 | |
|       setupLog.addAppender(
 | |
|         new lazy.Log.ConsoleAppender(new lazy.Log.BasicFormatter())
 | |
|       );
 | |
|       this._log = setupLog;
 | |
|     }
 | |
|     return this._log;
 | |
|   }
 | |
| 
 | |
|   registerSetupState(state) {
 | |
|     this.setupState.set(state.name, state);
 | |
|   }
 | |
| 
 | |
|   async observe(subject, topic, data) {
 | |
|     switch (topic) {
 | |
|       case lazy.UIState.ON_UPDATE:
 | |
|         this.logger.debug("Handling UIState update");
 | |
|         this.syncIsConnected = lazy.UIState.get().syncEnabled;
 | |
|         this.maybeUpdateUI();
 | |
|         break;
 | |
|       case TOPIC_DEVICELIST_UPDATED:
 | |
|         this.logger.debug("Handling observer notification:", topic, data);
 | |
|         this.refreshDevices();
 | |
|         this.maybeUpdateUI();
 | |
|         break;
 | |
|       case "fxaccounts:device_connected":
 | |
|       case "fxaccounts:device_disconnected":
 | |
|         await lazy.fxAccounts.device.refreshDeviceList();
 | |
|         this.maybeUpdateUI();
 | |
|         break;
 | |
|       case SYNC_SERVICE_ERROR:
 | |
|         this.syncIsWorking = false;
 | |
|         this.maybeUpdateUI();
 | |
|         break;
 | |
|       case NETWORK_STATUS_CHANGED:
 | |
|         this.networkIsOnline = data == "online";
 | |
|         this.maybeUpdateUI();
 | |
|         break;
 | |
|       case SYNC_SERVICE_FINISHED:
 | |
|         if (!this.syncIsWorking) {
 | |
|           this.syncIsWorking = true;
 | |
|           this.syncHasWorked = true;
 | |
|           this.maybeUpdateUI();
 | |
|         }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   refreshDevices() {
 | |
|     // compare new values to the previous values
 | |
|     const mobileDeviceConnected = this.mobileDeviceConnected;
 | |
|     const secondaryDeviceConnected = this.secondaryDeviceConnected;
 | |
| 
 | |
|     this.logger.debug(
 | |
|       `refreshDevices, mobileDeviceConnected: ${mobileDeviceConnected}, `,
 | |
|       `secondaryDeviceConnected: ${secondaryDeviceConnected}`
 | |
|     );
 | |
| 
 | |
|     let didDeviceStateChange =
 | |
|       this._deviceStateSnapshot.mobileDeviceConnected !=
 | |
|         mobileDeviceConnected ||
 | |
|       this._deviceStateSnapshot.secondaryDeviceConnected !=
 | |
|         secondaryDeviceConnected;
 | |
|     if (
 | |
|       mobileDeviceConnected &&
 | |
|       !this._deviceStateSnapshot.mobileDeviceConnected
 | |
|     ) {
 | |
|       // a mobile device was added, show success if we previously showed the promo
 | |
|       this._shouldShowSuccessConfirmation = this._didShowMobilePromo;
 | |
|     } else if (
 | |
|       !mobileDeviceConnected &&
 | |
|       this._deviceStateSnapshot.mobileDeviceConnected
 | |
|     ) {
 | |
|       // no mobile device connected now, reset
 | |
|       Services.prefs.clearUserPref(MOBILE_PROMO_DISMISSED_PREF);
 | |
|       this._shouldShowSuccessConfirmation = false;
 | |
|     }
 | |
|     this._deviceStateSnapshot = {
 | |
|       mobileDeviceConnected,
 | |
|       secondaryDeviceConnected,
 | |
|     };
 | |
|     if (didDeviceStateChange) {
 | |
|       this.logger.debug(
 | |
|         "refreshDevices: device state did change, call maybeUpdateUI"
 | |
|       );
 | |
|       if (!secondaryDeviceConnected) {
 | |
|         this.logger.debug(
 | |
|           "We lost a device, now claim sync hasn't worked before."
 | |
|         );
 | |
|         this.syncHasWorked = false;
 | |
|       }
 | |
|       this._uiUpdateNeeded = true;
 | |
|     } else {
 | |
|       this.logger.debug("refreshDevices: no device state change");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   maybeUpdateUI() {
 | |
|     let nextSetupStateName = this._currentSetupStateName;
 | |
|     let errorState = null;
 | |
| 
 | |
|     // state transition conditions
 | |
|     for (let state of this.setupState.values()) {
 | |
|       nextSetupStateName = state.name;
 | |
|       if (!state.exitConditions()) {
 | |
|         this.logger.debug(
 | |
|           "maybeUpdateUI, conditions not met to exit state: ",
 | |
|           nextSetupStateName
 | |
|         );
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let setupState = this.currentSetupState;
 | |
|     const state = this.setupState.get(nextSetupStateName);
 | |
|     const uiStateIndex = state.uiStateIndex;
 | |
| 
 | |
|     if (
 | |
|       uiStateIndex == 0 ||
 | |
|       nextSetupStateName != this._currentSetupStateName
 | |
|     ) {
 | |
|       setupState = state;
 | |
|       this._currentSetupStateName = nextSetupStateName;
 | |
|       this._uiUpdateNeeded = true;
 | |
|     }
 | |
|     this.logger.debug("maybeUpdateUI, _uiUpdateNeeded:", this._uiUpdateNeeded);
 | |
|     if (this._uiUpdateNeeded) {
 | |
|       this._uiUpdateNeeded = false;
 | |
|       if (this.shouldShowMobilePromo) {
 | |
|         this._didShowMobilePromo = true;
 | |
|       }
 | |
|       if (uiStateIndex == 0) {
 | |
|         errorState = this.getErrorType();
 | |
|       }
 | |
|       Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED, errorState);
 | |
|     }
 | |
|     if ("function" == typeof setupState.enter) {
 | |
|       setupState.enter();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   dismissMobilePromo() {
 | |
|     Services.prefs.setBoolPref(MOBILE_PROMO_DISMISSED_PREF, true);
 | |
|   }
 | |
| 
 | |
|   dismissMobileConfirmation() {
 | |
|     this._shouldShowSuccessConfirmation = false;
 | |
|     this._didShowMobilePromo = false;
 | |
|     this._uiUpdateNeeded = true;
 | |
|     this.maybeUpdateUI();
 | |
|   }
 | |
| 
 | |
|   async openFxASignup(window) {
 | |
|     const url = await lazy.fxAccounts.constructor.config.promiseConnectAccountURI(
 | |
|       "firefoxview"
 | |
|     );
 | |
|     openTabInWindow(window, url, true);
 | |
|     Services.telemetry.recordEvent("firefoxview", "fxa_continue", "sync", null);
 | |
|   }
 | |
| 
 | |
|   async openFxAPairDevice(window) {
 | |
|     const url = await lazy.fxAccounts.constructor.config.promisePairingURI({
 | |
|       entrypoint: "fx-view",
 | |
|     });
 | |
|     openTabInWindow(window, url, true);
 | |
|     Services.telemetry.recordEvent("firefoxview", "fxa_mobile", "sync", null, {
 | |
|       has_devices: this.secondaryDeviceConnected.toString(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   syncOpenTabs(containerElem) {
 | |
|     // Flip the pref on.
 | |
|     // The observer should trigger re-evaluating state and advance to next step
 | |
|     Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
 | |
|   }
 | |
| 
 | |
|   forceSyncTabs() {
 | |
|     lazy.SyncedTabs.syncTabs(true);
 | |
|   }
 | |
| })();
 | 
