Bug 1686343 - Ask user to pin Firefox during windows about:welcome onboarding r?pdahiya,andreio draft
authorEd Lee <edilee@mozilla.com>
Tue, 09 Feb 2021 20:42:43 -0800
changeset 3548180 342fb80989c51813b1218da57591924a5234e6aa
parent 3547296 d1b7430e5ebbe7a782c76959e2c8e1d87a5107c6
child 3548181 a9482c4ed9f58c346a08007daba61632d5a86da7
push id656940
push useredilee@gmail.com
push dateThu, 18 Feb 2021 18:30:28 +0000
treeherdertry@a9482c4ed9f5 [default view] [failures only]
reviewerspdahiya, andreio
bugs1686343
milestone87.0a1
Bug 1686343 - Ask user to pin Firefox during windows about:welcome onboarding r?pdahiya,andreio Support pin special action and add a new action property to wait for default browser that changes styles and content. Differential Revision: https://phabricator.services.mozilla.com/D105653
browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm
browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm
browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
browser/components/newtab/aboutwelcome/content/aboutwelcome.css
browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
browser/components/newtab/karma.mc.config.js
browser/components/newtab/test/unit/aboutwelcome/MultiStageAboutWelcome.test.jsx
toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
--- a/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm
+++ b/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm
@@ -193,16 +193,20 @@ class AboutWelcomeChild extends JSWindow
     Cu.exportFunction(this.AWGetSelectedTheme.bind(this), window, {
       defineAs: "AWGetSelectedTheme",
     });
 
     Cu.exportFunction(this.AWGetRegion.bind(this), window, {
       defineAs: "AWGetRegion",
     });
 
+    Cu.exportFunction(this.AWIsDefaultBrowser.bind(this), window, {
+      defineAs: "AWIsDefaultBrowser",
+    });
+
     Cu.exportFunction(this.AWSelectTheme.bind(this), window, {
       defineAs: "AWSelectTheme",
     });
 
     Cu.exportFunction(this.AWSendEventTelemetry.bind(this), window, {
       defineAs: "AWSendEventTelemetry",
     });
 
@@ -333,16 +337,20 @@ class AboutWelcomeChild extends JSWindow
   AWGetDefaultSites() {
     return this.wrapPromise(getDefaultSites(this));
   }
 
   AWGetSelectedTheme() {
     return this.wrapPromise(getSelectedTheme(this));
   }
 
+  AWIsDefaultBrowser() {
+    return this.wrapPromise(this.sendQuery("AWPage:IS_DEFAULT_BROWSER"));
+  }
+
   /**
    * Send Event Telemetry
    * @param {object} eventData
    */
   AWSendEventTelemetry(eventData) {
     this.AWSendToParent("TELEMETRY_EVENT", {
       ...eventData,
       event_context: {
--- a/browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm
+++ b/browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm
@@ -262,16 +262,18 @@ class AboutWelcomeParent extends JSWindo
       case "AWPage:GET_REGION":
         if (Region.home !== null) {
           return Region.home;
         }
         if (!this.RegionHomeObserver) {
           this.RegionHomeObserver = new RegionHomeObserver(this);
         }
         return this.RegionHomeObserver.promiseRegionHome();
+      case "AWPage:IS_DEFAULT_BROWSER":
+        return window.getShellService().isDefaultBrowser();
       case "AWPage:WAIT_FOR_MIGRATION_CLOSE":
         return new Promise(resolve =>
           Services.ww.registerNotification(function observer(subject, topic) {
             if (
               topic === "domwindowclosed" &&
               subject.document.documentURI ===
                 "chrome://browser/content/migration/migration.xhtml"
             ) {
--- a/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
+++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
@@ -448,16 +448,19 @@ const MultiStageAboutWelcome = props => 
       setActiveTheme: setActiveTheme
     }) : null;
   })));
 };
 class WelcomeScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.handleAction = this.handleAction.bind(this);
+    this.state = {
+      style: props.content.style || ""
+    };
   }
 
   handleOpenURL(action, flowParams, UTMTerm) {
     let {
       type,
       data
     } = action;
 
@@ -516,16 +519,33 @@ class WelcomeScreen extends react__WEBPA
       this.handleOpenURL(action, props.flowParams, props.UTMTerm);
     } else if (action.type) {
       _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].handleUserAction(action); // Wait until migration closes to complete the action
 
       if (action.type === "SHOW_MIGRATION_WIZARD") {
         await window.AWWaitForMigrationClose();
         _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].sendActionTelemetry(props.messageId, "migrate_close");
       }
+    } // Wait until we become default browser to continue rest of action.
+
+
+    if (action.waitForDefault) {
+      // Update the UI to show additional "waiting" content.
+      this.setState({
+        style: "waiting-for-default"
+      }); // Keep checking frequently as we want the UI to be responsive.
+
+      await new Promise(resolve => async function checkDefault() {
+        if (await window.AWIsDefaultBrowser()) {
+          resolve();
+        } else {
+          setTimeout(checkDefault, 100);
+        }
+      }());
+      _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_3__["AboutWelcomeUtils"].sendActionTelemetry(props.messageId, "default_browser");
     } // A special tiles.action.theme value indicates we should use the event's value vs provided value.
 
 
     if (action.theme) {
       let themeToUse = action.theme === "<event>" ? event.currentTarget.value : this.props.initialTheme || action.theme;
       this.props.setActiveTheme(themeToUse);
       window.AWSelectTheme(themeToUse);
     }
@@ -654,48 +674,50 @@ class WelcomeScreen extends react__WEBPA
         className: `indicator ${className}`
       }));
     }
 
     return steps;
   }
 
   renderHelpText() {
+    // Allow showing an alternate text based on the state or regular text.
+    const text = this.props.content.help_text[this.state.style] || this.props.content.help_text.text;
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
-      text: this.props.content.help_text.text
+      text: text
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("p", {
       id: "helptext",
       className: `helptext ${this.props.content.help_text.position}`
     }));
   }
 
   render() {
     const {
       content,
       topSites
     } = this.props;
     const showImportableSitesDisclaimer = content.tiles && content.tiles.type === "topsites" && topSites && topSites.showImportable;
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("main", {
-      className: `screen ${this.props.id}`
+      className: `screen ${this.props.id} ${this.state.style}`
     }, content.secondary_button_top ? this.renderSecondaryCTA("top") : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: `brand-logo ${content.secondary_button_top ? "cta-top" : ""}`
     }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: "welcome-text"
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_Zap__WEBPACK_IMPORTED_MODULE_2__["Zap"], {
       hasZap: content.zap,
       text: content.title
     }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
       text: content.subtitle
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", null))), content.tiles ? this.renderTiles() : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
       text: content.primary_button ? content.primary_button.label : null
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
       className: "primary",
       value: "primary_button",
       onClick: this.handleAction
-    }))), content.secondary_button ? this.renderSecondaryCTA() : null, content.help_text && content.help_text.position === "default" ? this.renderHelpText() : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("nav", {
+    }))), content.help_text && content.help_text.position === "default" ? this.renderHelpText() : null, content.secondary_button ? this.renderSecondaryCTA() : null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("nav", {
       className: content.help_text && content.help_text.position === "footer" || showImportableSitesDisclaimer ? "steps has-helptext" : "steps",
       "data-l10n-id": "onboarding-welcome-steps-indicator",
       "data-l10n-args": `{"current": ${parseInt(this.props.order, 10) + 1}, "total": ${this.props.totalNumberOfScreens}}`
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("br", null), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("p", null), this.renderStepsIndicator()), content.help_text && content.help_text.position === "footer" || showImportableSitesDisclaimer ? this.renderHelpText() : null);
   }
 
 }
 
--- a/browser/components/newtab/aboutwelcome/content/aboutwelcome.css
+++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.css
@@ -267,16 +267,26 @@ body {
   text-align: center;
   overflow-x: auto;
   height: 100vh;
   background-color: var(--newtab-background-color-1); }
   .onboardingContainer .screen {
     display: flex;
     flex-flow: column nowrap;
     height: 100%; }
+    .onboardingContainer .screen .delayed-tiles {
+      max-height: 500px;
+      overflow: hidden;
+      transition-property: max-height, opacity;
+      transition-duration: 0.4s; }
+    .onboardingContainer .screen.collapsed-tiles .delayed-tiles {
+      opacity: 0;
+      max-height: 0; }
+    .onboardingContainer .screen.waiting-for-default .primary {
+      display: none; }
   .onboardingContainer .brand-logo {
     background: url("chrome://branding/content/about-logo.svg") top center/112px no-repeat;
     padding: 112px 0 20px;
     margin-top: 60px; }
     .onboardingContainer .brand-logo.cta-top {
       margin-top: 25px; }
   .onboardingContainer .welcomeZap span {
     position: relative;
--- a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
+++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
@@ -251,16 +251,36 @@ body {
   overflow-x: auto;
   height: 100vh;
   background-color: var(--newtab-background-color-1);
 
   .screen {
     display: flex;
     flex-flow: column nowrap;
     height: 100%;
+
+    .delayed-tiles {
+      max-height: 500px;
+      overflow: hidden;
+      transition-property: max-height, opacity;
+      transition-duration: 0.4s;
+    }
+
+    &.collapsed-tiles {
+      .delayed-tiles {
+        opacity: 0;
+        max-height: 0;
+      }
+    }
+
+    &.waiting-for-default {
+      .primary {
+        display: none;
+      }
+    }
   }
 
   .brand-logo {
     background: url('chrome://branding/content/about-logo.svg') top
     center / $logo-size no-repeat;
     padding: $logo-size 0 20px;
     margin-top: 60px;
 
--- a/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
+++ b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
@@ -131,16 +131,19 @@ export const MultiStageAboutWelcome = pr
     </React.Fragment>
   );
 };
 
 export class WelcomeScreen extends React.PureComponent {
   constructor(props) {
     super(props);
     this.handleAction = this.handleAction.bind(this);
+    this.state = {
+      style: props.content.style || "",
+    };
   }
 
   handleOpenURL(action, flowParams, UTMTerm) {
     let { type, data } = action;
     if (type === "SHOW_FIREFOX_ACCOUNTS") {
       let params = {
         ...BASE_PARAMS,
         utm_term: `aboutwelcome-${UTMTerm}-screen`,
@@ -188,16 +191,35 @@ export class WelcomeScreen extends React
       AboutWelcomeUtils.handleUserAction(action);
       // Wait until migration closes to complete the action
       if (action.type === "SHOW_MIGRATION_WIZARD") {
         await window.AWWaitForMigrationClose();
         AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close");
       }
     }
 
+    // Wait until we become default browser to continue rest of action.
+    if (action.waitForDefault) {
+      // Update the UI to show additional "waiting" content.
+      this.setState({ style: "waiting-for-default" });
+
+      // Keep checking frequently as we want the UI to be responsive.
+      await new Promise(resolve =>
+        (async function checkDefault() {
+          if (await window.AWIsDefaultBrowser()) {
+            resolve();
+          } else {
+            setTimeout(checkDefault, 100);
+          }
+        })()
+      );
+
+      AboutWelcomeUtils.sendActionTelemetry(props.messageId, "default_browser");
+    }
+
     // A special tiles.action.theme value indicates we should use the event's value vs provided value.
     if (action.theme) {
       let themeToUse =
         action.theme === "<event>"
           ? event.currentTarget.value
           : this.props.initialTheme || action.theme;
 
       this.props.setActiveTheme(themeToUse);
@@ -367,18 +389,22 @@ export class WelcomeScreen extends React
     for (let i = 0; i < this.props.totalNumberOfScreens; i++) {
       let className = i === this.props.order ? "current" : "";
       steps.push(<div key={i} className={`indicator ${className}`} />);
     }
     return steps;
   }
 
   renderHelpText() {
+    // Allow showing an alternate text based on the state or regular text.
+    const text =
+      this.props.content.help_text[this.state.style] ||
+      this.props.content.help_text.text;
     return (
-      <Localized text={this.props.content.help_text.text}>
+      <Localized text={text}>
         <p
           id="helptext"
           className={`helptext ${this.props.content.help_text.position}`}
         />
       </Localized>
     );
   }
 
@@ -386,17 +412,17 @@ export class WelcomeScreen extends React
     const { content, topSites } = this.props;
     const showImportableSitesDisclaimer =
       content.tiles &&
       content.tiles.type === "topsites" &&
       topSites &&
       topSites.showImportable;
 
     return (
-      <main className={`screen ${this.props.id}`}>
+      <main className={`screen ${this.props.id} ${this.state.style}`}>
         {content.secondary_button_top ? this.renderSecondaryCTA("top") : null}
         <div
           className={`brand-logo ${
             content.secondary_button_top ? "cta-top" : ""
           }`}
         />
         <div className="welcome-text">
           <Zap hasZap={content.zap} text={content.title} />
@@ -411,20 +437,20 @@ export class WelcomeScreen extends React
           >
             <button
               className="primary"
               value="primary_button"
               onClick={this.handleAction}
             />
           </Localized>
         </div>
-        {content.secondary_button ? this.renderSecondaryCTA() : null}
         {content.help_text && content.help_text.position === "default"
           ? this.renderHelpText()
           : null}
+        {content.secondary_button ? this.renderSecondaryCTA() : null}
         <nav
           className={
             (content.help_text && content.help_text.position === "footer") ||
             showImportableSitesDisclaimer
               ? "steps has-helptext"
               : "steps"
           }
           data-l10n-id={"onboarding-welcome-steps-indicator"}
--- a/browser/components/newtab/karma.mc.config.js
+++ b/browser/components/newtab/karma.mc.config.js
@@ -184,20 +184,20 @@ module.exports = function(config) {
             },
             "content-src/lib/link-menu-options.js": {
               statements: 96,
               lines: 96,
               functions: 96,
               branches: 70,
             },
             "content-src/aboutwelcome/**/*.jsx": {
-              statements: 50,
-              lines: 50,
-              functions: 76,
-              branches: 0,
+              statements: 70,
+              lines: 69,
+              functions: 83,
+              branches: 43,
             },
             "content-src/components/**/*.jsx": {
               statements: 51.1,
               lines: 52.38,
               functions: 31.2,
               branches: 31.2,
             },
           },
--- a/browser/components/newtab/test/unit/aboutwelcome/MultiStageAboutWelcome.test.jsx
+++ b/browser/components/newtab/test/unit/aboutwelcome/MultiStageAboutWelcome.test.jsx
@@ -209,10 +209,81 @@ describe("MultiStageAboutWelcome module"
         );
         assert.isFalse(
           wrapper
             .find(".secondary button[value='secondary_button_top']")
             .exists()
         );
       });
     });
+    describe("#handleAction", () => {
+      let SCREEN_PROPS;
+      let TEST_ACTION;
+      beforeEach(() => {
+        SCREEN_PROPS = {
+          content: {
+            primary_button: {
+              action: {},
+              label: "test button",
+            },
+          },
+          navigate: sandbox.stub(),
+          setActiveTheme: sandbox.stub(),
+          UTMTerm: "you_tee_emm",
+        };
+        TEST_ACTION = SCREEN_PROPS.content.primary_button.action;
+        sandbox.stub(AboutWelcomeUtils, "handleUserAction");
+      });
+      it("should handle navigate", () => {
+        TEST_ACTION.navigate = true;
+        const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />);
+
+        wrapper.find(".primary").simulate("click");
+
+        assert.calledOnce(SCREEN_PROPS.navigate);
+      });
+      it("should handle theme", () => {
+        TEST_ACTION.theme = "test";
+        const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />);
+
+        wrapper.find(".primary").simulate("click");
+
+        assert.calledWith(SCREEN_PROPS.setActiveTheme, "test");
+      });
+      it("should handle SHOW_FIREFOX_ACCOUNTS", () => {
+        TEST_ACTION.type = "SHOW_FIREFOX_ACCOUNTS";
+        const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />);
+
+        wrapper.find(".primary").simulate("click");
+
+        assert.calledWith(AboutWelcomeUtils.handleUserAction, {
+          data: {
+            extraParams: {
+              utm_campaign: "firstrun",
+              utm_medium: "referral",
+              utm_source: "activity-stream",
+              utm_term: "aboutwelcome-you_tee_emm-screen",
+            },
+          },
+          type: "SHOW_FIREFOX_ACCOUNTS",
+        });
+      });
+      it("should handle SHOW_MIGRATION_WIZARD", () => {
+        TEST_ACTION.type = "SHOW_MIGRATION_WIZARD";
+        const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />);
+
+        wrapper.find(".primary").simulate("click");
+
+        assert.calledWith(AboutWelcomeUtils.handleUserAction, {
+          type: "SHOW_MIGRATION_WIZARD",
+        });
+      });
+      it("should handle waitForDefault", () => {
+        TEST_ACTION.waitForDefault = true;
+        const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />);
+
+        wrapper.find(".primary").simulate("click");
+
+        assert.propertyVal(wrapper.state(), "style", "waiting-for-default");
+      });
+    });
   });
 });
--- a/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
+++ b/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
@@ -75,16 +75,33 @@ const SpecialMessageActions = {
         install
       );
     } catch (e) {
       Cu.reportError(e);
     }
   },
 
   /**
+   * Pin Firefox to taskbar.
+   *
+   * @param {Window} window Reference to a window object
+   */
+  pinFirefoxToTaskbar(window) {
+    try {
+      // Currently this only works on certain Windows versions.
+      window
+        .getShellService()
+        .QueryInterface(Ci.nsIWindowsShellService)
+        .pinCurrentAppToTaskbar();
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+
+  /**
    *  Set browser as the operating system default browser.
    *
    *  @param {Window} window Reference to a window object
    */
   setDefaultBrowser(window) {
     window.getShellService().setAsDefault();
   },
 
@@ -224,16 +241,23 @@ const SpecialMessageActions = {
         break;
       case "INSTALL_ADDON_FROM_URL":
         await this.installAddonFromURL(
           browser,
           action.data.url,
           action.data.telemetrySource
         );
         break;
+      case "PIN_FIREFOX_TO_TASKBAR":
+        this.pinFirefoxToTaskbar(window);
+        break;
+      case "PIN_AND_DEFAULT":
+        this.pinFirefoxToTaskbar(window);
+        this.setDefaultBrowser(window);
+        break;
       case "SET_DEFAULT_BROWSER":
         this.setDefaultBrowser(window);
         break;
       case "PIN_CURRENT_TAB":
         let tab = window.gBrowser.selectedTab;
         window.gBrowser.pinTab(tab);
         window.ConfirmationHint.show(tab, "pinTab", {
           showDescription: true,