Bug 1591584 - Add dynamic triplet branch that shows sync and multidevice cards based off targeting r=k88hudson
authorPunam Dahiya <punamdahiya@yahoo.com>
Thu, 14 Nov 2019 01:04:23 +0000
changeset 501869 9fad2a91f180f123fa5770f5b2ecdeadc4f007a7
parent 501868 6e3c105cfcde3a6821120a83674f77692f5c7bf2
child 501870 06851b0adc9f3216b68177808ce0da19cd573fd9
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1591584
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1591584 - Add dynamic triplet branch that shows sync and multidevice cards based off targeting r=k88hudson iTest Plan: Differential Revision: https://phabricator.services.mozilla.com/D51471
browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/ASRouterTargeting.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/newtab/test/browser/browser_aboutwelcome.js
--- a/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
+++ b/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
@@ -558,28 +558,29 @@ declare const userPrefs: {
   cfrAddons: boolean;
   snippets: boolean;
 }
 ```
 
 ### `attachedFxAOAuthClients`
 
 Information about connected services associated with the FxA Account.
+Return an empty array if no account is found or an error occurs.
 
 #### Definition
 
 ```
 interface OAuthClient {
   id: string;
   // FxA service name
   name: string;
   lastAccessTime: UnixEpochNumber;
 }
 
-declare const attachedFxAOAuthClients: Array<OAuthClient>
+declare const attachedFxAOAuthClients: Promise<OAuthClient[]>
 ```
 
 #### Examples
 ```javascript
 {
   id: "7377719276ad44ee",
   name: "Pocket",
   lastAccessTime: 1513599164000
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -1223,29 +1223,30 @@ class _ASRouter {
     return ASRouterTargeting.findAllMatchingMessages({
       messages,
       trigger,
       context,
       onError: this._handleTargetingError,
     });
   }
 
-  _findMessage(candidateMessages, trigger) {
+  _findMessage(candidateMessages, trigger, ordered = false) {
     const messages = candidateMessages.filter(m =>
       this.isBelowFrequencyCaps(m)
     );
     const context = this._getMessagesContext();
 
     // Find a message that matches the targeting context as well as the trigger context (if one is provided)
     // If no trigger is provided, we should find a message WITHOUT a trigger property defined.
     return ASRouterTargeting.findMatchingMessage({
       messages,
       trigger,
       context,
       onError: this._handleTargetingError,
+      ordered,
     });
   }
 
   async evaluateExpression(target, { expression, context }) {
     const channel = target || this.messageChannel;
     let evaluationStatus;
     try {
       evaluationStatus = {
@@ -1345,17 +1346,18 @@ class _ASRouter {
           break;
         }
       }
     } else {
       while (bundledMessagesOfSameTemplate.length) {
         // Find a message that matches the targeting context - or break if there are no matching messages
         const message = await this._findMessage(
           bundledMessagesOfSameTemplate,
-          trigger
+          trigger,
+          true
         );
         if (!message) {
           /* istanbul ignore next */ // Code coverage in mochitests
           break;
         }
         // Only copy the content of the message (that's what the UI cares about)
         // Also delete the message we picked so we don't pick it again
         result.push({
--- a/browser/components/newtab/lib/ASRouterTargeting.jsm
+++ b/browser/components/newtab/lib/ASRouterTargeting.jsm
@@ -212,16 +212,28 @@ const QueryCache = {
     }),
     TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"),
     CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(),
     RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"),
   },
 };
 
 /**
+ * sortMessagesByOrder
+ *
+ * Each message has an associated order, which is guaranteed to be strictly
+ * positive. Sort the messages so that message shows in order specified
+ *
+ */
+
+function sortMessagesByOrder(messages) {
+  return messages.sort((a, b) => a.order - b.order);
+}
+
+/**
  * sortMessagesByWeightedRank
  *
  * Each message has an associated weight, which is guaranteed to be strictly
  * positive. Sort the messages so that higher weighted messages are more likely
  * to come first.
  *
  * Specifically, sort them so that the probability of message x_1 with weight
  * w_1 appearing before message x_2 with weight w_2 is (w_1 / (w_1 + w_2)).
@@ -457,17 +469,28 @@ const TargetingGetters = {
       cfrAddons: cfrAddonsUserPref,
       snippets: snippetsUserPref,
     };
   },
   get totalBlockedCount() {
     return TrackingDBService.sumAllEvents();
   },
   get attachedFxAOAuthClients() {
-    return this.usesFirefoxSync ? fxAccounts.listAttachedOAuthClients() : [];
+    // Explicitly catch error objects e.g.  NO_ACCOUNT triggered when
+    // setting FXA_USERNAME_PREF from tests
+    return this.usesFirefoxSync
+      ? new Promise(resolve => {
+          fxAccounts
+            .listAttachedOAuthClients()
+            .then(clients => {
+              resolve(clients);
+            })
+            .catch(() => resolve([]));
+        })
+      : [];
   },
   get platformName() {
     return AppConstants.platform;
   },
 };
 
 this.ASRouterTargeting = {
   Environment: TargetingGetters,
@@ -563,18 +586,20 @@ this.ASRouterTargeting = {
           : this.ERROR_TYPES.OTHER_ERROR;
         onError(type, error, message);
       }
       result = false;
     }
     return result;
   },
 
-  _getSortedMessages(messages) {
-    const weightSortedMessages = sortMessagesByWeightedRank([...messages]);
+  _getSortedMessages(messages, ordered) {
+    const weightSortedMessages = ordered
+      ? sortMessagesByOrder(messages)
+      : sortMessagesByWeightedRank([...messages]);
     const sortedMessages = sortMessagesByTargeting(weightSortedMessages);
     return sortMessagesByPriority(sortedMessages);
   },
 
   _getCombinedContext(trigger, context) {
     const triggerContext = trigger ? trigger.context : {};
     return this.combineContexts(context, triggerContext);
   },
@@ -594,20 +619,27 @@ this.ASRouterTargeting = {
   /**
    * findMatchingMessage - Given an array of messages, returns one message
    *                       whos targeting expression evaluates to true
    *
    * @param {Array} messages An array of AS router messages
    * @param {trigger} string A trigger expression if a message for that trigger is desired
    * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
    * @param {func} onError A function to handle errors (takes two params; error, message)
+   * @param {func} ordered An optional param when true sort message by order specified in message
    * @returns {obj} an AS router message
    */
-  async findMatchingMessage({ messages, trigger, context, onError }) {
-    const sortedMessages = this._getSortedMessages(messages);
+  async findMatchingMessage({
+    messages,
+    trigger,
+    context,
+    onError,
+    ordered = false,
+  }) {
+    const sortedMessages = this._getSortedMessages(messages, ordered);
     const combinedContext = this._getCombinedContext(trigger, context);
 
     for (const candidate of sortedMessages) {
       if (
         await this._isMessageMatch(candidate, trigger, combinedContext, onError)
       ) {
         return candidate;
       }
@@ -618,20 +650,27 @@ this.ASRouterTargeting = {
   /**
    * findAllMatchingMessages - Given an array of messages, returns an array of
    *                           messages that that match the targeting.
    *
    * @param {Array} messages An array of AS router messages.
    * @param {trigger} string A trigger expression if a message for that trigger is desired.
    * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
    * @param {func} onError A function to handle errors (takes two params; error, message)
+   * @param {func} ordered An optional param when true sort message by order specified in message
    * @returns {Array} An array of AS router messages that match.
    */
-  async findAllMatchingMessages({ messages, trigger, context, onError }) {
-    const sortedMessages = this._getSortedMessages(messages);
+  async findAllMatchingMessages({
+    messages,
+    trigger,
+    context,
+    onError,
+    ordered = false,
+  }) {
+    const sortedMessages = this._getSortedMessages(messages, ordered);
     const combinedContext = this._getCombinedContext(trigger, context);
     const matchingMessages = [];
 
     for (const candidate of sortedMessages) {
       if (
         await this._isMessageMatch(candidate, trigger, combinedContext, onError)
       ) {
         matchingMessages.push(candidate);
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -263,17 +263,18 @@ const ONBOARDING_MESSAGES = () => [
           data: {
             args:
               "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=activity-stream-firstrun&style=trailhead",
             where: "tabshifted",
           },
         },
       },
     },
-    targeting: "trailheadTriplet == 'supercharge'",
+    targeting:
+      "trailheadTriplet == 'supercharge' || (trailheadTriplet == 'dynamic' && usesFirefoxSync == false)",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_3",
     template: "onboarding",
     bundled: 3,
     order: 2,
     content: {
@@ -283,34 +284,35 @@ const ONBOARDING_MESSAGES = () => [
       primary_button: {
         label: { string_id: "onboarding-firefox-monitor-button" },
         action: {
           type: "OPEN_URL",
           data: { args: "https://monitor.firefox.com/", where: "tabshifted" },
         },
       },
     },
-    targeting: "trailheadTriplet in ['payoff', 'supercharge']",
+    targeting:
+      "trailheadTriplet == 'supercharge' || (trailheadTriplet == 'dynamic' && !('Firefox Monitor' in attachedFxAOAuthClients|mapToProperty('name')))",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_4",
     template: "onboarding",
     bundled: 3,
-    order: 1,
+    order: 3,
     content: {
       title: { string_id: "onboarding-browse-privately-title" },
       text: { string_id: "onboarding-browse-privately-text" },
       icon: "private",
       primary_button: {
         label: { string_id: "onboarding-browse-privately-button" },
         action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
       },
     },
-    targeting: "trailheadTriplet == 'privacy'",
+    targeting: "trailheadTriplet == 'dynamic'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_5",
     template: "onboarding",
     bundled: 3,
     order: 5,
     content: {
@@ -327,57 +329,58 @@ const ONBOARDING_MESSAGES = () => [
     },
     targeting: "trailheadTriplet == 'payoff'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_6",
     template: "onboarding",
     bundled: 3,
-    order: 3,
+    order: 6,
     content: {
       title: { string_id: "onboarding-mobile-phone-title" },
       text: { string_id: "onboarding-mobile-phone-text" },
       icon: "mobile",
       primary_button: {
         label: { string_id: "onboarding-mobile-phone-button" },
         action: {
           type: "OPEN_URL",
           data: {
             args: "https://www.mozilla.org/firefox/mobile/",
             where: "tabshifted",
           },
         },
       },
     },
-    targeting: "trailheadTriplet in ['supercharge', 'multidevice']",
+    targeting:
+      "trailheadTriplet == 'supercharge' || (trailheadTriplet == 'dynamic' && sync.mobileDevices < 1)",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_7",
     template: "onboarding",
     bundled: 3,
-    order: 3,
+    order: 4,
     content: {
       title: { string_id: "onboarding-send-tabs-title" },
       text: { string_id: "onboarding-send-tabs-text" },
       icon: "sendtab",
       primary_button: {
         label: { string_id: "onboarding-send-tabs-button" },
         action: {
           type: "OPEN_URL",
           data: {
             args:
               "https://support.mozilla.org/kb/send-tab-firefox-desktop-other-devices",
             where: "tabshifted",
           },
         },
       },
     },
-    targeting: "trailheadTriplet == 'multidevice'",
+    targeting: "trailheadTriplet == 'dynamic'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_8",
     template: "onboarding",
     bundled: 3,
     order: 2,
     content: {
@@ -397,30 +400,30 @@ const ONBOARDING_MESSAGES = () => [
     },
     targeting: "trailheadTriplet == 'multidevice'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_9",
     template: "onboarding",
     bundled: 3,
-    order: 3,
+    order: 7,
     content: {
       title: { string_id: "onboarding-lockwise-passwords-title" },
       text: { string_id: "onboarding-lockwise-passwords-text2" },
       icon: "lockwise",
       primary_button: {
         label: { string_id: "onboarding-lockwise-passwords-button2" },
         action: {
           type: "OPEN_URL",
           data: { args: "https://lockwise.firefox.com/", where: "tabshifted" },
         },
       },
     },
-    targeting: "trailheadTriplet == 'privacy'",
+    targeting: "trailheadTriplet == 'dynamic'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_10",
     template: "onboarding",
     bundled: 3,
     order: 4,
     content: {
--- a/browser/components/newtab/test/browser/browser_aboutwelcome.js
+++ b/browser/components/newtab/test/browser/browser_aboutwelcome.js
@@ -65,24 +65,51 @@ async function test_trailhead_branch(
   BrowserTestUtils.removeTab(tab);
 }
 
 /**
  * Test the the various trailhead branches.
  */
 add_task(async function test_trailhead_branches() {
   await test_trailhead_branch(
-    "join-privacy",
+    "join-dynamic",
+    // Expected selectors:
+    [
+      ".trailhead.joinCohort",
+      "button[data-l10n-id=onboarding-data-sync-button2]",
+      "button[data-l10n-id=onboarding-firefox-monitor-button]",
+      "button[data-l10n-id=onboarding-browse-privately-button]",
+    ]
+  );
+
+  // Validate sync card is not shown if user usesFirefoxSync
+  await pushPrefs(["services.sync.username", "someone@foo.com"]);
+  await test_trailhead_branch(
+    "join-dynamic",
     // Expected selectors:
     [
       ".trailhead.joinCohort",
+      "button[data-l10n-id=onboarding-firefox-monitor-button]",
       "button[data-l10n-id=onboarding-browse-privately-button]",
-      "button[data-l10n-id=onboarding-tracking-protection-button2]",
-      "button[data-l10n-id=onboarding-lockwise-passwords-button2]",
-    ]
+    ],
+    // Unexpected selectors:
+    ["button[data-l10n-id=onboarding-data-sync-button2]"]
+  );
+
+  // Validate multidevice card is not shown if user has mobile devices connected
+  await pushPrefs(["services.sync.clients.devices.mobile", 1]);
+  await test_trailhead_branch(
+    "join-dynamic",
+    // Expected selectors:
+    [
+      ".trailhead.joinCohort",
+      "button[data-l10n-id=onboarding-firefox-monitor-button]",
+    ],
+    // Unexpected selectors:
+    ["button[data-l10n-id=onboarding-mobile-phone-button"]
   );
 
   await test_trailhead_branch(
     "sync-supercharge",
     // Expected selectors:
     [
       ".trailhead.syncCohort",
       "button[data-l10n-id=onboarding-data-sync-button2]",
@@ -135,37 +162,14 @@ add_task(async function test_trailhead_b
       ".trailheadCard",
       "p[data-l10n-id=onboarding-benefit-products-text]",
       "button[data-l10n-id=onboarding-join-form-continue]",
       "button[data-l10n-id=onboarding-join-form-signin]",
     ]
   );
 
   await test_trailhead_branch(
-    "cards-multidevice",
-    // Expected selectors:
-    [
-      "button[data-l10n-id=onboarding-mobile-phone-button]",
-      "button[data-l10n-id=onboarding-pocket-anywhere-button]",
-      "button[data-l10n-id=onboarding-send-tabs-button]",
-    ],
-    // Unexpected selectors:
-    ["#trailheadDialog"]
-  );
-
-  await test_trailhead_branch(
-    "join-payoff",
-    // Expected selectors:
-    [
-      ".trailhead.joinCohort",
-      "button[data-l10n-id=onboarding-firefox-monitor-button]",
-      "button[data-l10n-id=onboarding-facebook-container-button]",
-      "button[data-l10n-id=onboarding-firefox-send-button]",
-    ]
-  );
-
-  await test_trailhead_branch(
     "nofirstrun",
     [],
     // Unexpected selectors:
     ["#trailheadDialog", ".trailheadCards"]
   );
 });