From 886cd169542d8e70e147dab1b79334f9e075fcca Mon Sep 17 00:00:00 2001 From: Sam Foster Date: Sat, 30 Jul 2022 01:30:15 +0000 Subject: [PATCH] Bug 1776779 - Add mobile promo and success confirmation banner. r=Gijs,markh * Add mobile promo element to the Fxa/sync setup flow, and logic to show/hide it * Add success confirmation for the mobile sync connection with logic to show/hide it * Watch a new pref 'browser.tabs.firefox-view.mobilePromo.dismissed' for the promo * Add a new notification in FxAccountsDevice when the devicelist cache is updated * Use the devicelist updated notification drive the state changes in the setup flow manager * Add test coverage for the mobile promo Differential Revision: https://phabricator.services.mozilla.com/D151895 --- .../firefox-view-tabs-setup-manager.sys.mjs | 234 ++++-- .../components/firefoxview/firefoxView.ftl | 11 + .../components/firefoxview/firefoxview.css | 63 +- .../components/firefoxview/firefoxview.html | 25 +- .../firefoxview/tab-pickup-container.mjs | 93 ++- .../tests/browser/browser_firefoxview_tab.js | 40 - .../tests/browser/browser_setup_state.js | 684 +++++++++++++----- .../firefoxview/tests/browser/head.js | 73 ++ services/fxaccounts/FxAccountsCommon.js | 1 + services/fxaccounts/FxAccountsDevice.jsm | 2 + .../test_accounts_device_registration.js | 50 ++ 11 files changed, 960 insertions(+), 316 deletions(-) diff --git a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs index 364af7a1786f..a4ffe6d3b56f 100644 --- a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs +++ b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs @@ -15,6 +15,7 @@ const { XPCOMUtils } = ChromeUtils.importESModule( const lazy = {}; XPCOMUtils.defineLazyModuleGetters(lazy, { + Log: "resource://gre/modules/Log.jsm", UIState: "resource://services-sync/UIState.jsm", SyncedTabs: "resource://services-sync/SyncedTabs.jsm", }); @@ -26,7 +27,11 @@ XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => { 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"; function openTabInWindow(window, url) { const { @@ -40,31 +45,7 @@ export const TabsSetupFlowManager = new (class { this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]); this.setupState = new Map(); - this._currentSetupStateName = ""; - - Services.obs.addObserver(this, lazy.UIState.ON_UPDATE); - Services.obs.addObserver(this, "fxaccounts:device_connected"); - Services.obs.addObserver(this, "fxaccounts:device_disconnected"); - - // this.syncTabsPrefEnabled will track the value of the tabs pref - XPCOMUtils.defineLazyPreferenceGetter( - this, - "syncTabsPrefEnabled", - SYNC_TABS_PREF, - false, - () => { - this.maybeUpdateUI(); - } - ); - XPCOMUtils.defineLazyPreferenceGetter( - this, - "lastTabFetch", - RECENT_TABS_SYNC, - false, - () => { - this.maybeUpdateUI(); - } - ); + this.resetInternalState(); this.registerSetupState({ uiStateIndex: 0, @@ -76,7 +57,7 @@ export const TabsSetupFlowManager = new (class { // TODO: handle offline, sync service not ready or available this.registerSetupState({ uiStateIndex: 1, - name: "connect-mobile-device", + name: "connect-secondary-device", exitConditions: () => { return this.secondaryDeviceConnected; }, @@ -109,26 +90,81 @@ export const TabsSetupFlowManager = new (class { }, }); - // attempting to resolve the fxa user is a proxy for readiness - lazy.fxAccounts.getSignedInUser().then(() => { - this.maybeUpdateUI(); - }); + Services.obs.addObserver(this, lazy.UIState.ON_UPDATE); + Services.obs.addObserver(this, TOPIC_DEVICELIST_UPDATED); + + // 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, + false, + () => { + 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; + + // keep track of what is connected so we can respond to changes + this._deviceStateSnapshot = { + mobileDeviceConnected: this.mobileDeviceConnected, + secondaryDeviceConnected: this.secondaryDeviceConnected, + }; } uninit() { Services.obs.removeObserver(this, lazy.UIState.ON_UPDATE); - Services.obs.removeObserver(this, "fxaccounts:device_connected"); - Services.obs.removeObserver(this, "fxaccounts:device_disconnected"); + Services.obs.removeObserver(this, TOPIC_DEVICELIST_UPDATED); + } + get currentSetupState() { + return this.setupState.get(this._currentSetupStateName); } get uiStateIndex() { - const state = - this._currentSetupStateName && - this.setupState.get(this._currentSetupStateName); - return state ? state.uiStateIndex : -1; + return this.currentSetupState.uiStateIndex; } get fxaSignedIn() { - return lazy.UIState.get().status === lazy.UIState.STATUS_SIGNED_IN; + 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; } @@ -139,6 +175,41 @@ export const TabsSetupFlowManager = new (class { 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.currentSetupState.uiStateIndex >= 3 && + !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); } @@ -146,16 +217,60 @@ export const TabsSetupFlowManager = new (class { async observe(subject, topic, data) { switch (topic) { case lazy.UIState.ON_UPDATE: + this.logger.debug("Handling UIState update"); this.maybeUpdateUI(); break; - case "fxaccounts:device_connected": - case "fxaccounts:device_disconnected": - await lazy.fxAccounts.device.refreshDeviceList(); + case TOPIC_DEVICELIST_UPDATED: + this.logger.debug("Handling observer notification:", topic, data); + this.refreshDevices(); this.maybeUpdateUI(); break; } } + 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" + ); + this._uiUpdateNeeded = true; + } else { + this.logger.debug("refreshDevices: no device state change"); + } + } + maybeUpdateUI() { let nextSetupStateName = this._currentSetupStateName; @@ -167,19 +282,34 @@ export const TabsSetupFlowManager = new (class { } } - if (nextSetupStateName !== this._currentSetupStateName) { - const state = this.setupState.get(nextSetupStateName); + let setupState = this.currentSetupState; + if (nextSetupStateName != this._currentSetupStateName) { + setupState = this.setupState.get(nextSetupStateName); this._currentSetupStateName = nextSetupStateName; - - Services.obs.notifyObservers( - null, - TOPIC_SETUPSTATE_CHANGED, - state.uiStateIndex - ); - if ("function" == typeof state.enter) { - state.enter(); - } + this._uiUpdateNeeded = true; } + this.logger.debug("maybeUpdateUI, _uiUpdateNeeded:", this._uiUpdateNeeded); + if (this._uiUpdateNeeded) { + this._uiUpdateNeeded = false; + if (this.shouldShowMobilePromo) { + this._didShowMobilePromo = true; + } + Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED); + } + 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) { @@ -188,10 +318,12 @@ export const TabsSetupFlowManager = new (class { ); openTabInWindow(window, url, true); } + openSyncPreferences(window) { const url = "about:preferences?action=pair#sync"; openTabInWindow(window, url, true); } + syncOpenTabs(containerElem) { // Flip the pref on. // The observer should trigger re-evaluating state and advance to next step diff --git a/browser/components/firefoxview/firefoxView.ftl b/browser/components/firefoxview/firefoxView.ftl index 946855244e62..667af7d37781 100644 --- a/browser/components/firefoxview/firefoxView.ftl +++ b/browser/components/firefoxview/firefoxView.ftl @@ -14,6 +14,10 @@ menu-tools-firefox-view = firefoxview-page-title = { -firefoxview-brand-name } +firefoxview-close-button = + .title = Close + .aria-label = Close + # Used instead of the localized relative time when a timestamp is within a minute or so of now firefoxview-just-now-timestamp = Just now @@ -43,6 +47,13 @@ firefoxview-tabpickup-synctabs-primarybutton = Sync open tabs firefoxview-tabpickup-syncing = Sit tight while your tabs sync. It’ll be just a moment. +firefoxview-mobile-promo-header = Grab tabs from your phone or tablet +firefoxview-mobile-promo-description = To view your latest mobile tabs, sign in to { -brand-product-name } on iOS or Android. +firefoxview-mobile-promo-primarybutton = Get { -brand-product-name } for mobile + +firefoxview-mobile-confirmation-header = 🎉 Good to go! +firefoxview-mobile-confirmation-description = Now you can grab your { -brand-product-name } tabs from your tablet or phone. + firefoxview-closed-tabs-title = Recently closed firefoxview-closed-tabs-collapse-button = .title = Show or hide recently closed tabs list diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css index 58dbad4c35ef..fa3ae696aa7f 100644 --- a/browser/components/firefoxview/firefoxview.css +++ b/browser/components/firefoxview/firefoxview.css @@ -8,6 +8,11 @@ --sidebar-width: 200px; --header-spacing: 40px; --footer-spacing: 80px; + --card-border-zap-gradient: linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%); + + --success-background-color: #87FFD1; + --success-border-color: #2AC3A2; + --success-color: #15141A; --colorways-figure-size: 225px; --colorways-grid-template-areas: @@ -254,6 +259,17 @@ body > main > aside { display: none !important; } +button.primary { + white-space: nowrap; + min-width: fit-content; +} + +button.close { + background-image: url(chrome://global/skin/icons/close.svg); + -moz-context-properties: fill; + fill: currentColor; +} + .card, .synced-tab-li, .synced-tab-li-placeholder, @@ -306,12 +322,12 @@ body > main > aside { } /* Bug 1770534 - Only use the zap-gradient for built-in, color-neutral themes */ -.setup-step { +.zap-card { border: none; position: relative; z-index: 0; } -.setup-step::before { +.zap-card::before { content: ""; position: absolute; z-index: -1; @@ -339,11 +355,6 @@ body > main > aside { flex: 1 1 auto; } -.setup-step > .step-body > button.primary { - white-space: nowrap; - min-width: fit-content; -} - .setup-step > footer { display: flex; flex-direction: column; @@ -354,6 +365,44 @@ body > main > aside { margin-block: 0 8px; } +.message-box { + display: flex; + align-content: space-between; + align-items: center; + margin-block: 8px; + gap: 8px; +} + +.message-content { + flex: 1 1 auto; +} + +.message-content > .message-header { + font-size: 1.12em; + margin-block: 0 0.33em; +} + +.message-content > .message-description { + margin-block: 0 0.33em; +} + +.confirmation-message-box { + background-color: var(--success-background-color); + color: var(--success-color); + border-color: var(--success-border-color); +} +.confirmation-message-box > .message-content { + text-align: center; +} +.confirmation-message-box > .message-content > .message-header { + font-size: inherit; + display: inline; +} +.confirmation-message-box > .icon-button { + /* ensure we get the local color value as container doesnt change color with theme */ + color: inherit; +} + /* 117px is the total height of the collapsible-tabs-container; setting that size for the second row stabilizes the layout so it doesn't shift when collapsibled */ #recently-closed-tabs-container { diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html index 7ae4da380a49..5576a03f5287 100644 --- a/browser/components/firefoxview/firefoxview.html +++ b/browser/components/firefoxview/firefoxview.html @@ -33,7 +33,7 @@