/* 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 SyncedTabsErrorHandler singleton, which handles * error states for synced tabs. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { UIState: "resource://services-sync/UIState.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "syncUtils", () => { return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs") .Utils; }); XPCOMUtils.defineLazyServiceGetter( lazy, "gNetworkLinkService", "@mozilla.org/network/network-link-service;1", "nsINetworkLinkService" ); const FXA_ENABLED = "identity.fxaccounts.enabled"; const NETWORK_STATUS_CHANGED = "network:offline-status-changed"; const SYNC_SERVICE_ERROR = "weave:service:sync:error"; const SYNC_SERVICE_FINISHED = "weave:service:sync:finish"; const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed"; const ErrorType = Object.freeze({ SYNC_ERROR: "sync-error", FXA_ADMIN_DISABLED: "fxa-admin-disabled", NETWORK_OFFLINE: "network-offline", SYNC_DISCONNECTED: "sync-disconnected", PASSWORD_LOCKED: "password-locked", SIGNED_OUT: "signed-out", }); export const SyncedTabsErrorHandler = { init() { this.networkIsOnline = lazy.gNetworkLinkService.linkStatusKnown && lazy.gNetworkLinkService.isLinkUp; this.syncIsConnected = lazy.UIState.get().syncEnabled; this.syncIsWorking = true; Services.obs.addObserver(this, NETWORK_STATUS_CHANGED); Services.obs.addObserver(this, lazy.UIState.ON_UPDATE); Services.obs.addObserver(this, SYNC_SERVICE_ERROR); Services.obs.addObserver(this, SYNC_SERVICE_FINISHED); Services.obs.addObserver(this, TOPIC_DEVICESTATE_CHANGED); return this; }, 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 ); }, getErrorType() { // this ordering is important for dealing with multiple errors at once const errorStates = { [ErrorType.NETWORK_OFFLINE]: !this.networkIsOnline, [ErrorType.FXA_ADMIN_DISABLED]: Services.prefs.prefIsLocked(FXA_ENABLED), [ErrorType.PASSWORD_LOCKED]: this.isPrimaryPasswordLocked, [ErrorType.SIGNED_OUT]: lazy.UIState.get().status === lazy.UIState.STATUS_LOGIN_FAILED, [ErrorType.SYNC_DISCONNECTED]: !this.syncIsConnected, [ErrorType.SYNC_ERROR]: !this.syncIsWorking && !this.syncHasWorked, }; for (let [type, value] of Object.entries(errorStates)) { if (value) { return type; } } return null; }, getFluentStringsForErrorType(type) { return Object.freeze(this._errorStateStringMappings[type]); }, get isPrimaryPasswordLocked() { return lazy.syncUtils.mpLocked(); }, isSyncReady() { 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) ); }, observe(_, topic, data) { switch (topic) { case NETWORK_STATUS_CHANGED: this.networkIsOnline = data == "online"; break; case lazy.UIState.ON_UPDATE: this.syncIsConnected = lazy.UIState.get().syncEnabled; break; case SYNC_SERVICE_ERROR: if (lazy.UIState.get().status == lazy.UIState.STATUS_SIGNED_IN) { this.syncIsWorking = false; } break; case SYNC_SERVICE_FINISHED: if (!this.syncIsWorking) { this.syncIsWorking = true; this.syncHasWorked = true; } break; case TOPIC_DEVICESTATE_CHANGED: this.syncHasWorked = false; } }, ErrorType, // We map the error state strings to Fluent string IDs so that it's easier // to change strings in the future without having to update all of the // error state strings. _errorStateStringMappings: { [ErrorType.SYNC_ERROR]: { header: "firefoxview-tabpickup-sync-error-header", description: "firefoxview-tabpickup-generic-sync-error-description", buttonLabel: "firefoxview-tabpickup-sync-error-primarybutton", }, [ErrorType.FXA_ADMIN_DISABLED]: { header: "firefoxview-tabpickup-fxa-admin-disabled-header", description: "firefoxview-tabpickup-fxa-disabled-by-policy-description", // The button is hidden for this errorState, so we don't include the // buttonLabel property. }, [ErrorType.NETWORK_OFFLINE]: { header: "firefoxview-tabpickup-network-offline-header", description: "firefoxview-tabpickup-network-offline-description", buttonLabel: "firefoxview-tabpickup-network-offline-primarybutton", }, [ErrorType.SYNC_DISCONNECTED]: { header: "firefoxview-tabpickup-sync-disconnected-header", description: "firefoxview-tabpickup-sync-disconnected-description", buttonLabel: "firefoxview-tabpickup-sync-disconnected-primarybutton", }, [ErrorType.PASSWORD_LOCKED]: { header: "firefoxview-tabpickup-password-locked-header", description: "firefoxview-tabpickup-password-locked-description", buttonLabel: "firefoxview-tabpickup-password-locked-primarybutton", link: { label: "firefoxview-tabpickup-password-locked-link", href: Services.urlFormatter.formatURLPref("app.support.baseURL") + "primary-password-stored-logins", }, }, [ErrorType.SIGNED_OUT]: { header: "firefoxview-tabpickup-signed-out-header", description: "firefoxview-tabpickup-signed-out-description2", buttonLabel: "firefoxview-tabpickup-signed-out-primarybutton", }, }, }.init();