forked from mirrors/gecko-dev
		
	* Keep track of the tab-pickup-container views in TabsSetupFlowManager and their visibility * Fix visibilitychange handling in tab-pickup-container and add some tests * Capture a timestamp when a device has been added and there are 0 tabs to show, with at least one visible tab-pickup-container view * Record telemetry when there are > 0 tabs in these conditions * Small change to rename the `_waitingForTabs` internal tracking property to `_waitingForNextTabSync` to better clarify its use and meaning * Use a consistent pattern in some of the existing tests with how we mock SyncedTabs.getRecentTabs Differential Revision: https://phabricator.services.mozilla.com/D170526
		
			
				
	
	
		
			712 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			712 lines
		
	
	
	
		
			23 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/. */
 | 
						|
 | 
						|
/**
 | 
						|
 * This module exports the TabsSetupFlowManager singleton, which manages the state and
 | 
						|
 * diverse inputs which drive the Firefox View synced tabs setup flow
 | 
						|
 */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  Log: "resource://gre/modules/Log.sys.mjs",
 | 
						|
  SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
 | 
						|
  UIState: "resource://services-sync/UIState.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(lazy, "syncUtils", () => {
 | 
						|
  return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
 | 
						|
    .Utils;
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
 | 
						|
  return ChromeUtils.importESModule(
 | 
						|
    "resource://gre/modules/FxAccounts.sys.mjs"
 | 
						|
  ).getFxAccountsSingleton();
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  lazy,
 | 
						|
  "gNetworkLinkService",
 | 
						|
  "@mozilla.org/network/network-link-service;1",
 | 
						|
  "nsINetworkLinkService"
 | 
						|
);
 | 
						|
 | 
						|
const SYNC_TABS_PREF = "services.sync.engine.tabs";
 | 
						|
const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
 | 
						|
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 FXA_DEVICE_CONNECTED = "fxaccounts:device_connected";
 | 
						|
const FXA_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
 | 
						|
const SYNC_SERVICE_FINISHED = "weave:service:sync:finish";
 | 
						|
const PRIMARY_PASSWORD_UNLOCKED = "passwordmgr-crypto-login";
 | 
						|
const TAB_PICKUP_OPEN_STATE_PREF =
 | 
						|
  "browser.tabs.firefox-view.ui-state.tab-pickup.open";
 | 
						|
 | 
						|
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.didFxaTabOpen = false;
 | 
						|
 | 
						|
    this.registerSetupState({
 | 
						|
      uiStateIndex: 0,
 | 
						|
      name: "error-state",
 | 
						|
      exitConditions: () => {
 | 
						|
        const fxaStatus = lazy.UIState.get().status;
 | 
						|
        return (
 | 
						|
          this.networkIsOnline &&
 | 
						|
          (this.syncIsWorking || this.syncHasWorked) &&
 | 
						|
          !Services.prefs.prefIsLocked(FXA_ENABLED) &&
 | 
						|
          // it's an error for sync to not be connected if we are signed-in,
 | 
						|
          // or for sync to be connected if the FxA status is "login_failed",
 | 
						|
          // which can happen if a user updates their password on another device
 | 
						|
          ((!this.syncIsConnected &&
 | 
						|
            fxaStatus !== lazy.UIState.STATUS_SIGNED_IN) ||
 | 
						|
            (this.syncIsConnected &&
 | 
						|
              fxaStatus !== lazy.UIState.STATUS_LOGIN_FAILED)) &&
 | 
						|
          // We treat a locked primary password as an error if we are signed-in.
 | 
						|
          // If the user dismisses the prompt to unlock, they can use the "Try again" button to prompt again
 | 
						|
          (!this.isPrimaryPasswordLocked || !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-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);
 | 
						|
    Services.obs.addObserver(this, TOPIC_TABS_CHANGED);
 | 
						|
    Services.obs.addObserver(this, PRIMARY_PASSWORD_UNLOCKED);
 | 
						|
    Services.obs.addObserver(this, FXA_DEVICE_CONNECTED);
 | 
						|
    Services.obs.addObserver(this, FXA_DEVICE_DISCONNECTED);
 | 
						|
 | 
						|
    // this.syncTabsPrefEnabled will track the value of the tabs pref
 | 
						|
    XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
      this,
 | 
						|
      "syncTabsPrefEnabled",
 | 
						|
      SYNC_TABS_PREF,
 | 
						|
      false,
 | 
						|
      () => {
 | 
						|
        this.maybeUpdateUI(true);
 | 
						|
      }
 | 
						|
    );
 | 
						|
    XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
      this,
 | 
						|
      "mobilePromoDismissedPref",
 | 
						|
      MOBILE_PROMO_DISMISSED_PREF,
 | 
						|
      false,
 | 
						|
      () => {
 | 
						|
        this.maybeUpdateUI(true);
 | 
						|
      }
 | 
						|
    );
 | 
						|
 | 
						|
    this._lastFxASignedIn = this.fxaSignedIn;
 | 
						|
    this.logger.debug(
 | 
						|
      "TabsSetupFlowManager constructor, fxaSignedIn:",
 | 
						|
      this._lastFxASignedIn
 | 
						|
    );
 | 
						|
    this.onSignedInChange();
 | 
						|
  }
 | 
						|
 | 
						|
  resetInternalState() {
 | 
						|
    // assign initial values for all the managed internal properties
 | 
						|
    delete this._lastFxASignedIn;
 | 
						|
    this._currentSetupStateName = "not-signed-in";
 | 
						|
    this._shouldShowSuccessConfirmation = false;
 | 
						|
    this._didShowMobilePromo = false;
 | 
						|
    this.abortWaitingForTabs();
 | 
						|
 | 
						|
    this.syncHasWorked = false;
 | 
						|
 | 
						|
    // keep track of what is connected so we can respond to changes
 | 
						|
    this._deviceStateSnapshot = {
 | 
						|
      mobileDeviceConnected: this.mobileDeviceConnected,
 | 
						|
      secondaryDeviceConnected: this.secondaryDeviceConnected,
 | 
						|
    };
 | 
						|
    // keep track of tab-pickup-container instance visibilities
 | 
						|
    this._viewVisibilityStates = new Map();
 | 
						|
  }
 | 
						|
 | 
						|
  get isPrimaryPasswordLocked() {
 | 
						|
    return lazy.syncUtils.mpLocked();
 | 
						|
  }
 | 
						|
 | 
						|
  getErrorType() {
 | 
						|
    // this ordering is important for dealing with multiple errors at once
 | 
						|
    const errorStates = {
 | 
						|
      "network-offline": !this.networkIsOnline,
 | 
						|
      "fxa-admin-disabled": Services.prefs.prefIsLocked(FXA_ENABLED),
 | 
						|
      "password-locked": this.isPrimaryPasswordLocked,
 | 
						|
      "signed-out":
 | 
						|
        lazy.UIState.get().status === lazy.UIState.STATUS_LOGIN_FAILED,
 | 
						|
      "sync-disconnected": !this.syncIsConnected,
 | 
						|
      "sync-error": !this.syncIsWorking && !this.syncHasWorked,
 | 
						|
    };
 | 
						|
 | 
						|
    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);
 | 
						|
    Services.obs.removeObserver(this, TOPIC_TABS_CHANGED);
 | 
						|
    Services.obs.removeObserver(this, PRIMARY_PASSWORD_UNLOCKED);
 | 
						|
    Services.obs.removeObserver(this, FXA_DEVICE_CONNECTED);
 | 
						|
    Services.obs.removeObserver(this, FXA_DEVICE_DISCONNECTED);
 | 
						|
  }
 | 
						|
  get hasVisibleViews() {
 | 
						|
    return Array.from(this._viewVisibilityStates.values()).reduce(
 | 
						|
      (hasVisible, visibility) => {
 | 
						|
        return hasVisible || visibility == "visible";
 | 
						|
      },
 | 
						|
      false
 | 
						|
    );
 | 
						|
  }
 | 
						|
  get currentSetupState() {
 | 
						|
    return this.setupState.get(this._currentSetupStateName);
 | 
						|
  }
 | 
						|
  get isTabSyncSetupComplete() {
 | 
						|
    return this.currentSetupState.uiStateIndex >= 4;
 | 
						|
  }
 | 
						|
  get uiStateIndex() {
 | 
						|
    return this.currentSetupState.uiStateIndex;
 | 
						|
  }
 | 
						|
  get fxaSignedIn() {
 | 
						|
    let { UIState } = lazy;
 | 
						|
    let syncState = UIState.get();
 | 
						|
    return (
 | 
						|
      UIState.isReady() &&
 | 
						|
      syncState.status === UIState.STATUS_SIGNED_IN &&
 | 
						|
      // syncEnabled just checks the "services.sync.username" pref has a value
 | 
						|
      syncState.syncEnabled
 | 
						|
    );
 | 
						|
  }
 | 
						|
  get secondaryDeviceConnected() {
 | 
						|
    if (!this.fxaSignedIn) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    let recentDevices = lazy.fxAccounts.device?.recentDeviceList?.length;
 | 
						|
    return recentDevices > 1;
 | 
						|
  }
 | 
						|
  get mobileDeviceConnected() {
 | 
						|
    if (!this.fxaSignedIn) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    let mobileClients = lazy.fxAccounts.device.recentDeviceList?.filter(
 | 
						|
      device => device.type == "mobile" || device.type == "tablet"
 | 
						|
    );
 | 
						|
    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;
 | 
						|
        if (this._lastFxASignedIn !== this.fxaSignedIn) {
 | 
						|
          this.onSignedInChange();
 | 
						|
        } else {
 | 
						|
          this.maybeUpdateUI();
 | 
						|
        }
 | 
						|
        this._lastFxASignedIn = this.fxaSignedIn;
 | 
						|
        break;
 | 
						|
      case TOPIC_DEVICELIST_UPDATED:
 | 
						|
        this.logger.debug("Handling observer notification:", topic, data);
 | 
						|
        const { deviceStateChanged, deviceAdded } = await this.refreshDevices();
 | 
						|
        if (deviceStateChanged) {
 | 
						|
          this.maybeUpdateUI(true);
 | 
						|
        }
 | 
						|
        if (deviceAdded && this.secondaryDeviceConnected) {
 | 
						|
          this.logger.debug("device was added");
 | 
						|
          this._deviceAddedResultsNeverSeen = true;
 | 
						|
          if (this.hasVisibleViews) {
 | 
						|
            this.startWaitingForNewDeviceTabs();
 | 
						|
          }
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case FXA_DEVICE_CONNECTED:
 | 
						|
      case FXA_DEVICE_DISCONNECTED:
 | 
						|
        await lazy.fxAccounts.device.refreshDeviceList({ ignoreCached: true });
 | 
						|
        this.maybeUpdateUI(true);
 | 
						|
        break;
 | 
						|
      case SYNC_SERVICE_ERROR:
 | 
						|
        this.logger.debug(`Handling ${SYNC_SERVICE_ERROR}`);
 | 
						|
        if (lazy.UIState.get().status == lazy.UIState.STATUS_SIGNED_IN) {
 | 
						|
          this.abortWaitingForTabs();
 | 
						|
          this.syncIsWorking = false;
 | 
						|
          this.maybeUpdateUI(true);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case NETWORK_STATUS_CHANGED:
 | 
						|
        this.networkIsOnline = data == "online";
 | 
						|
        this.abortWaitingForTabs();
 | 
						|
        this.maybeUpdateUI(true);
 | 
						|
        break;
 | 
						|
      case SYNC_SERVICE_FINISHED:
 | 
						|
        this.logger.debug(`Handling ${SYNC_SERVICE_FINISHED}`);
 | 
						|
        // We intentionally leave any empty-tabs timestamp
 | 
						|
        // as we may be still waiting for a sync that delivers some tabs
 | 
						|
        this._waitingForNextTabSync = false;
 | 
						|
        if (!this.syncIsWorking) {
 | 
						|
          this.syncIsWorking = true;
 | 
						|
          this.syncHasWorked = true;
 | 
						|
        }
 | 
						|
        this.maybeUpdateUI(true);
 | 
						|
        break;
 | 
						|
      case TOPIC_TABS_CHANGED:
 | 
						|
        this.stopWaitingForTabs();
 | 
						|
        break;
 | 
						|
      case PRIMARY_PASSWORD_UNLOCKED:
 | 
						|
        this.logger.debug(`Handling ${PRIMARY_PASSWORD_UNLOCKED}`);
 | 
						|
        this.tryToClearError();
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  updateViewVisibility(instanceId, visibility) {
 | 
						|
    const wasVisible = this.hasVisibleViews;
 | 
						|
    this.logger.debug(
 | 
						|
      `updateViewVisibility for instance: ${instanceId}, visibility: ${visibility}`
 | 
						|
    );
 | 
						|
    if (visibility == "unloaded") {
 | 
						|
      this._viewVisibilityStates.delete(instanceId);
 | 
						|
    } else {
 | 
						|
      this._viewVisibilityStates.set(instanceId, visibility);
 | 
						|
    }
 | 
						|
    const isVisible = this.hasVisibleViews;
 | 
						|
    if (isVisible && !wasVisible) {
 | 
						|
      // If we're already timing waiting for tabs from a newly-added device
 | 
						|
      // we might be able to stop
 | 
						|
      if (this._noTabsVisibleFromAddedDeviceTimestamp) {
 | 
						|
        return this.stopWaitingForNewDeviceTabs();
 | 
						|
      }
 | 
						|
      if (this._deviceAddedResultsNeverSeen) {
 | 
						|
        // If this is the first time a view has been visible since a device was added
 | 
						|
        // we may want to start the empty-tabs visible timer
 | 
						|
        return this.startWaitingForNewDeviceTabs();
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (!isVisible) {
 | 
						|
      this.logger.debug(
 | 
						|
        "Resetting timestamp and tabs pending flags as there are no visible views"
 | 
						|
      );
 | 
						|
      // if there's no view visible, we're not really waiting anymore
 | 
						|
      this.abortWaitingForTabs();
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  get waitingForTabs() {
 | 
						|
    return (
 | 
						|
      // signed in & at least 1 other device is syncing indicates there's something to wait for
 | 
						|
      this.secondaryDeviceConnected && this._waitingForNextTabSync
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  abortWaitingForTabs() {
 | 
						|
    this._waitingForNextTabSync = false;
 | 
						|
    // also clear out the device-added / tabs pending flags
 | 
						|
    this._noTabsVisibleFromAddedDeviceTimestamp = 0;
 | 
						|
    this._deviceAddedResultsNeverSeen = false;
 | 
						|
  }
 | 
						|
 | 
						|
  startWaitingForTabs() {
 | 
						|
    if (!this._waitingForNextTabSync) {
 | 
						|
      this._waitingForNextTabSync = true;
 | 
						|
      Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async stopWaitingForTabs() {
 | 
						|
    const wasWaiting = this.waitingForTabs;
 | 
						|
    if (this.hasVisibleViews && this._deviceAddedResultsNeverSeen) {
 | 
						|
      await this.stopWaitingForNewDeviceTabs();
 | 
						|
    }
 | 
						|
    this._waitingForNextTabSync = false;
 | 
						|
    if (wasWaiting) {
 | 
						|
      Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async onSignedInChange() {
 | 
						|
    this.logger.debug("onSignedInChange, fxaSignedIn:", this.fxaSignedIn);
 | 
						|
    // update UI to make the state change
 | 
						|
    this.maybeUpdateUI(true);
 | 
						|
    if (!this.fxaSignedIn) {
 | 
						|
      // As we just signed out, ensure the waiting flag is reset for next time around
 | 
						|
      this.abortWaitingForTabs();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Set Tab pickup open state pref to true when signing in
 | 
						|
    Services.prefs.setBoolPref(TAB_PICKUP_OPEN_STATE_PREF, true);
 | 
						|
 | 
						|
    // Now we need to figure out if we have recently synced tabs to show
 | 
						|
    // Or, if we are going to need to trigger a tab sync for them
 | 
						|
    const recentTabs = await lazy.SyncedTabs.getRecentTabs(50);
 | 
						|
 | 
						|
    if (!this.fxaSignedIn) {
 | 
						|
      // We got signed-out in the meantime. We should get an ON_UPDATE which will put us
 | 
						|
      // back in the right state, so we just do nothing here
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // When SyncedTabs has resolved the getRecentTabs promise,
 | 
						|
    // we also know we can update devices-related internal state
 | 
						|
    const { deviceStateChanged } = await this.refreshDevices();
 | 
						|
    if (deviceStateChanged) {
 | 
						|
      this.logger.debug(
 | 
						|
        "onSignedInChange, after refreshDevices, calling maybeUpdateUI"
 | 
						|
      );
 | 
						|
      // give the UI an opportunity to update as secondaryDeviceConnected or
 | 
						|
      // mobileDeviceConnected have changed value
 | 
						|
      this.maybeUpdateUI(true);
 | 
						|
    }
 | 
						|
 | 
						|
    // If we can't get recent tabs, we need to trigger a request for them
 | 
						|
    const tabSyncNeeded = !recentTabs?.length;
 | 
						|
    this.logger.debug("onSignedInChange, tabSyncNeeded:", tabSyncNeeded);
 | 
						|
 | 
						|
    if (tabSyncNeeded) {
 | 
						|
      this.startWaitingForTabs();
 | 
						|
      this.logger.debug(
 | 
						|
        "isPrimaryPasswordLocked:",
 | 
						|
        this.isPrimaryPasswordLocked
 | 
						|
      );
 | 
						|
      this.logger.debug("onSignedInChange, no recentTabs, calling syncTabs");
 | 
						|
      // If the syncTabs call rejects or resolves false we need to clear the waiting
 | 
						|
      // flag and update UI
 | 
						|
      this.syncTabs()
 | 
						|
        .catch(ex => {
 | 
						|
          this.logger.debug("onSignedInChange, syncTabs rejected:", ex);
 | 
						|
          this.stopWaitingForTabs();
 | 
						|
        })
 | 
						|
        .then(willSync => {
 | 
						|
          if (!willSync) {
 | 
						|
            this.logger.debug("onSignedInChange, no tab sync expected");
 | 
						|
            this.stopWaitingForTabs();
 | 
						|
          }
 | 
						|
        });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async startWaitingForNewDeviceTabs() {
 | 
						|
    // if we're already waiting for tabs, don't reset
 | 
						|
    if (this._noTabsVisibleFromAddedDeviceTimestamp) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // take a timestamp whenever the latest device is added and we have 0 tabs to show,
 | 
						|
    // allowing us to track how long we show an empty list after a new device is added
 | 
						|
    const hasRecentTabs = (await lazy.SyncedTabs.getRecentTabs(1)).length;
 | 
						|
    if (this.hasVisibleViews && !hasRecentTabs) {
 | 
						|
      this._noTabsVisibleFromAddedDeviceTimestamp = Date.now();
 | 
						|
      this.logger.debug(
 | 
						|
        "New device added with 0 synced tabs to show, storing timestamp:",
 | 
						|
        this._noTabsVisibleFromAddedDeviceTimestamp
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async stopWaitingForNewDeviceTabs() {
 | 
						|
    if (!this._noTabsVisibleFromAddedDeviceTimestamp) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    const recentTabs = await lazy.SyncedTabs.getRecentTabs(1);
 | 
						|
    if (recentTabs.length) {
 | 
						|
      // We have been waiting for > 0 tabs after a newly-added device, record
 | 
						|
      // the time elapsed
 | 
						|
      const elapsed = Date.now() - this._noTabsVisibleFromAddedDeviceTimestamp;
 | 
						|
      this.logger.debug(
 | 
						|
        "stopWaitingForTabs, resetting _noTabsVisibleFromAddedDeviceTimestamp and recording telemetry:",
 | 
						|
        Math.round(elapsed / 1000)
 | 
						|
      );
 | 
						|
      this._noTabsVisibleFromAddedDeviceTimestamp = 0;
 | 
						|
      this._deviceAddedResultsNeverSeen = false;
 | 
						|
      Services.telemetry.recordEvent(
 | 
						|
        "firefoxview",
 | 
						|
        "synced_tabs_empty",
 | 
						|
        "since_device_added",
 | 
						|
        Math.round(elapsed / 1000).toString()
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      // we are still waiting for some tabs to show...
 | 
						|
      this.logger.debug(
 | 
						|
        "stopWaitingForTabs: Still no recent tabs, we are still waiting"
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async refreshDevices() {
 | 
						|
    // If current device not found in recent device list, refresh device list
 | 
						|
    if (
 | 
						|
      !lazy.fxAccounts.device.recentDeviceList?.some(
 | 
						|
        device => device.isCurrentDevice
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      await lazy.fxAccounts.device.refreshDeviceList({ ignoreCached: true });
 | 
						|
    }
 | 
						|
 | 
						|
    // compare new values to the previous values
 | 
						|
    const mobileDeviceConnected = this.mobileDeviceConnected;
 | 
						|
    const secondaryDeviceConnected = this.secondaryDeviceConnected;
 | 
						|
    const oldDevicesCount = this._deviceStateSnapshot?.devicesCount ?? 0;
 | 
						|
    const devicesCount = lazy.fxAccounts.device?.recentDeviceList?.length ?? 0;
 | 
						|
 | 
						|
    this.logger.debug(
 | 
						|
      `refreshDevices, mobileDeviceConnected: ${mobileDeviceConnected}, `,
 | 
						|
      `secondaryDeviceConnected: ${secondaryDeviceConnected}`
 | 
						|
    );
 | 
						|
 | 
						|
    let deviceStateChanged =
 | 
						|
      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,
 | 
						|
      devicesCount,
 | 
						|
    };
 | 
						|
    if (deviceStateChanged) {
 | 
						|
      this.logger.debug("refreshDevices: device state did change");
 | 
						|
      if (!secondaryDeviceConnected) {
 | 
						|
        this.logger.debug(
 | 
						|
          "We lost a device, now claim sync hasn't worked before."
 | 
						|
        );
 | 
						|
        this.syncHasWorked = false;
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      this.logger.debug("refreshDevices: no device state change");
 | 
						|
    }
 | 
						|
    return {
 | 
						|
      deviceStateChanged,
 | 
						|
      deviceAdded: oldDevicesCount < devicesCount,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  maybeUpdateUI(forceUpdate = false) {
 | 
						|
    let nextSetupStateName = this._currentSetupStateName;
 | 
						|
    let errorState = null;
 | 
						|
    let stateChanged = false;
 | 
						|
 | 
						|
    // 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;
 | 
						|
      stateChanged = true;
 | 
						|
    }
 | 
						|
    this.logger.debug(
 | 
						|
      "maybeUpdateUI, will notify update?:",
 | 
						|
      stateChanged,
 | 
						|
      forceUpdate
 | 
						|
    );
 | 
						|
    if (stateChanged || forceUpdate) {
 | 
						|
      if (this.shouldShowMobilePromo) {
 | 
						|
        this._didShowMobilePromo = true;
 | 
						|
      }
 | 
						|
      if (uiStateIndex == 0) {
 | 
						|
        errorState = this.getErrorType();
 | 
						|
        this.logger.debug("maybeUpdateUI, in error state:", errorState);
 | 
						|
      }
 | 
						|
      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.maybeUpdateUI(true);
 | 
						|
  }
 | 
						|
 | 
						|
  async openFxASignup(window) {
 | 
						|
    if (!(await lazy.fxAccounts.constructor.canConnectAccount())) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    const url = await lazy.fxAccounts.constructor.config.promiseConnectAccountURI(
 | 
						|
      "fx-view"
 | 
						|
    );
 | 
						|
    this.didFxaTabOpen = true;
 | 
						|
    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",
 | 
						|
    });
 | 
						|
    this.didFxaTabOpen = true;
 | 
						|
    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);
 | 
						|
  }
 | 
						|
 | 
						|
  async syncOnPageReload() {
 | 
						|
    if (lazy.UIState.isReady() && this.fxaSignedIn) {
 | 
						|
      this.startWaitingForTabs();
 | 
						|
      await this.syncTabs(true);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  tryToClearError() {
 | 
						|
    if (lazy.UIState.isReady() && this.fxaSignedIn) {
 | 
						|
      this.startWaitingForTabs();
 | 
						|
      if (this.isPrimaryPasswordLocked) {
 | 
						|
        lazy.syncUtils.ensureMPUnlocked();
 | 
						|
      }
 | 
						|
      this.logger.debug("tryToClearError: triggering new tab sync");
 | 
						|
      this.syncTabs();
 | 
						|
      Services.tm.dispatchToMainThread(() => {});
 | 
						|
    } else {
 | 
						|
      this.logger.debug(
 | 
						|
        `tryToClearError: unable to sync, isReady: ${lazy.UIState.isReady()}, fxaSignedIn: ${
 | 
						|
          this.fxaSignedIn
 | 
						|
        }`
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
  // For easy overriding in tests
 | 
						|
  syncTabs(force = false) {
 | 
						|
    return lazy.SyncedTabs.syncTabs(force);
 | 
						|
  }
 | 
						|
})();
 |