1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4"use strict";
5
6const EXPORTED_SYMBOLS = ["AboutWelcomeDefaults", "DEFAULT_WELCOME_CONTENT"];
7
8const { XPCOMUtils } = ChromeUtils.import(
9  "resource://gre/modules/XPCOMUtils.jsm"
10);
11
12XPCOMUtils.defineLazyModuleGetters(this, {
13  AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
14  AppConstants: "resource://gre/modules/AppConstants.jsm",
15  AttributionCode: "resource:///modules/AttributionCode.jsm",
16  Services: "resource://gre/modules/Services.jsm",
17});
18
19const DEFAULT_WELCOME_CONTENT = {
20  template: "multistage",
21  screens: [
22    {
23      id: "AW_SET_DEFAULT",
24      order: 0,
25      content: {
26        zap: true,
27        title: {
28          string_id: "onboarding-multistage-set-default-header",
29        },
30        subtitle: {
31          string_id: "onboarding-multistage-set-default-subtitle",
32        },
33        primary_button: {
34          label: {
35            string_id: "onboarding-multistage-set-default-primary-button-label",
36          },
37          action: {
38            navigate: true,
39            type: "SET_DEFAULT_BROWSER",
40          },
41        },
42        secondary_button: {
43          label: {
44            string_id:
45              "onboarding-multistage-set-default-secondary-button-label",
46          },
47          action: {
48            navigate: true,
49          },
50        },
51        secondary_button_top: {
52          text: {
53            string_id: "onboarding-multistage-welcome-secondary-button-text",
54          },
55          label: {
56            string_id: "onboarding-multistage-welcome-secondary-button-label",
57          },
58          action: {
59            data: {
60              entrypoint: "activity-stream-firstrun",
61            },
62            type: "SHOW_FIREFOX_ACCOUNTS",
63            addFlowParams: true,
64          },
65        },
66      },
67    },
68    {
69      id: "AW_IMPORT_SETTINGS",
70      order: 1,
71      content: {
72        zap: true,
73        help_text: {
74          text: {
75            string_id: "onboarding-import-sites-disclaimer",
76          },
77        },
78        title: {
79          string_id: "onboarding-multistage-import-header",
80        },
81        subtitle: {
82          string_id: "onboarding-multistage-import-subtitle",
83        },
84        tiles: {
85          type: "topsites",
86          showTitles: true,
87        },
88        primary_button: {
89          label: {
90            string_id: "onboarding-multistage-import-primary-button-label",
91          },
92          action: {
93            type: "SHOW_MIGRATION_WIZARD",
94            navigate: true,
95          },
96        },
97        secondary_button: {
98          label: {
99            string_id: "onboarding-multistage-import-secondary-button-label",
100          },
101          action: {
102            navigate: true,
103          },
104        },
105      },
106    },
107    {
108      id: "AW_CHOOSE_THEME",
109      order: 2,
110      content: {
111        zap: true,
112        title: {
113          string_id: "onboarding-multistage-theme-header",
114        },
115        subtitle: {
116          string_id: "onboarding-multistage-theme-subtitle",
117        },
118        tiles: {
119          type: "theme",
120          action: {
121            theme: "<event>",
122          },
123          data: [
124            {
125              theme: "automatic",
126              label: {
127                string_id: "onboarding-multistage-theme-label-automatic",
128              },
129              tooltip: {
130                string_id: "onboarding-multistage-theme-tooltip-automatic-2",
131              },
132              description: {
133                string_id:
134                  "onboarding-multistage-theme-description-automatic-2",
135              },
136            },
137            {
138              theme: "light",
139              label: {
140                string_id: "onboarding-multistage-theme-label-light",
141              },
142              tooltip: {
143                string_id: "onboarding-multistage-theme-tooltip-light-2",
144              },
145              description: {
146                string_id: "onboarding-multistage-theme-description-light",
147              },
148            },
149            {
150              theme: "dark",
151              label: {
152                string_id: "onboarding-multistage-theme-label-dark",
153              },
154              tooltip: {
155                string_id: "onboarding-multistage-theme-tooltip-dark-2",
156              },
157              description: {
158                string_id: "onboarding-multistage-theme-description-dark",
159              },
160            },
161            {
162              theme: "alpenglow",
163              label: {
164                string_id: "onboarding-multistage-theme-label-alpenglow",
165              },
166              tooltip: {
167                string_id: "onboarding-multistage-theme-tooltip-alpenglow-2",
168              },
169              description: {
170                string_id: "onboarding-multistage-theme-description-alpenglow",
171              },
172            },
173          ],
174        },
175        primary_button: {
176          label: {
177            string_id: "onboarding-multistage-theme-primary-button-label2",
178          },
179          action: {
180            navigate: true,
181          },
182        },
183        secondary_button: {
184          label: {
185            string_id: "onboarding-multistage-theme-secondary-button-label",
186          },
187          action: {
188            theme: "automatic",
189            navigate: true,
190          },
191        },
192      },
193    },
194  ],
195};
196
197const DEFAULT_PROTON_WELCOME_CONTENT = {
198  id: "DEFAULT_ABOUTWELCOME_PROTON",
199  template: "multistage",
200  transitions: true,
201  background_url:
202    "chrome://activity-stream/content/data/content/assets/proton-bkg.jpg",
203  screens: [
204    {
205      id: "AW_PIN_FIREFOX",
206      order: 0,
207      content: {
208        title: {
209          string_id: "mr1-onboarding-pin-header",
210        },
211        subtitle: {
212          string_id: "mr1-welcome-screen-hero-text",
213        },
214        help_text: {
215          text: {
216            string_id: "mr1-onboarding-welcome-image-caption",
217          },
218        },
219        primary_button: {
220          label: {
221            string_id: "mr1-onboarding-pin-primary-button-label",
222          },
223          action: {
224            navigate: true,
225            type: "PIN_FIREFOX_TO_TASKBAR",
226          },
227        },
228        secondary_button: {
229          label: {
230            string_id: "mr1-onboarding-set-default-secondary-button-label",
231          },
232          action: {
233            navigate: true,
234          },
235        },
236        secondary_button_top: {
237          label: {
238            string_id: "mr1-onboarding-sign-in-button-label",
239          },
240          action: {
241            data: {
242              entrypoint: "activity-stream-firstrun",
243            },
244            type: "SHOW_FIREFOX_ACCOUNTS",
245            addFlowParams: true,
246          },
247        },
248      },
249    },
250    {
251      id: "AW_SET_DEFAULT",
252      order: 1,
253      content: {
254        title: {
255          string_id: "mr1-onboarding-default-header",
256        },
257        subtitle: {
258          string_id: "mr1-onboarding-default-subtitle",
259        },
260        primary_button: {
261          label: {
262            string_id: "mr1-onboarding-default-primary-button-label",
263          },
264          action: {
265            navigate: true,
266            type: "SET_DEFAULT_BROWSER",
267          },
268        },
269        secondary_button: {
270          label: {
271            string_id: "mr1-onboarding-set-default-secondary-button-label",
272          },
273          action: {
274            navigate: true,
275          },
276        },
277      },
278    },
279    {
280      id: "AW_IMPORT_SETTINGS",
281      order: 2,
282      content: {
283        title: {
284          string_id: "mr1-onboarding-import-header",
285        },
286        subtitle: {
287          string_id: "mr1-onboarding-import-subtitle",
288        },
289        primary_button: {
290          label: {
291            string_id:
292              "mr1-onboarding-import-primary-button-label-no-attribution",
293          },
294          action: {
295            type: "SHOW_MIGRATION_WIZARD",
296            data: {},
297            navigate: true,
298          },
299        },
300        secondary_button: {
301          label: {
302            string_id: "mr1-onboarding-import-secondary-button-label",
303          },
304          action: {
305            navigate: true,
306          },
307        },
308      },
309    },
310    {
311      id: "AW_CHOOSE_THEME",
312      order: 3,
313      content: {
314        title: {
315          string_id: "mr1-onboarding-theme-header",
316        },
317        subtitle: {
318          string_id: "mr1-onboarding-theme-subtitle",
319        },
320        tiles: {
321          type: "theme",
322          action: {
323            theme: "<event>",
324          },
325          data: [
326            {
327              theme: "automatic",
328              label: {
329                string_id: "mr1-onboarding-theme-label-system",
330              },
331              tooltip: {
332                string_id: "mr1-onboarding-theme-tooltip-system",
333              },
334              description: {
335                string_id: "mr1-onboarding-theme-description-system",
336              },
337            },
338            {
339              theme: "light",
340              label: {
341                string_id: "mr1-onboarding-theme-label-light",
342              },
343              tooltip: {
344                string_id: "mr1-onboarding-theme-tooltip-light",
345              },
346              description: {
347                string_id: "mr1-onboarding-theme-description-light",
348              },
349            },
350            {
351              theme: "dark",
352              label: {
353                string_id: "mr1-onboarding-theme-label-dark",
354              },
355              tooltip: {
356                string_id: "mr1-onboarding-theme-tooltip-dark",
357              },
358              description: {
359                string_id: "mr1-onboarding-theme-description-dark",
360              },
361            },
362            {
363              theme: "alpenglow",
364              label: {
365                string_id: "mr1-onboarding-theme-label-alpenglow",
366              },
367              tooltip: {
368                string_id: "mr1-onboarding-theme-tooltip-alpenglow",
369              },
370              description: {
371                string_id: "mr1-onboarding-theme-description-alpenglow",
372              },
373            },
374          ],
375        },
376        primary_button: {
377          label: {
378            string_id: "mr1-onboarding-theme-primary-button-label",
379          },
380          action: {
381            navigate: true,
382          },
383        },
384        secondary_button: {
385          label: {
386            string_id: "mr1-onboarding-theme-secondary-button-label",
387          },
388          action: {
389            theme: "automatic",
390            navigate: true,
391          },
392        },
393      },
394    },
395  ],
396};
397
398async function getAddonFromRepository(data) {
399  const [addonInfo] = await AddonRepository.getAddonsByIDs([data]);
400  if (addonInfo.sourceURI.scheme !== "https") {
401    return null;
402  }
403  return {
404    name: addonInfo.name,
405    url: addonInfo.sourceURI.spec,
406    iconURL: addonInfo.icons["64"] || addonInfo.icons["32"],
407  };
408}
409
410async function getAddonInfo(attrbObj) {
411  let { content, source } = attrbObj;
412  try {
413    if (!content || source !== "addons.mozilla.org") {
414      return null;
415    }
416    // Attribution data can be double encoded
417    while (content.includes("%")) {
418      try {
419        const result = decodeURIComponent(content);
420        if (result === content) {
421          break;
422        }
423        content = result;
424      } catch (e) {
425        break;
426      }
427    }
428    // return_to_amo embeds the addon id in the content
429    // param, prefixed with "rta:".  Translating that
430    // happens in AddonRepository, however we can avoid
431    // an API call if we check up front here.
432    if (content.startsWith("rta:")) {
433      return await getAddonFromRepository(content);
434    }
435  } catch (e) {
436    Cu.reportError("Failed to get the latest add-on version for Return to AMO");
437  }
438  return null;
439}
440
441async function getAttributionContent() {
442  let attribution = await AttributionCode.getAttrDataAsync();
443  if (attribution?.source === "addons.mozilla.org") {
444    let addonInfo = await getAddonInfo(attribution);
445    if (addonInfo) {
446      return {
447        ...addonInfo,
448        template: "return_to_amo",
449      };
450    }
451  }
452  if (attribution?.ua) {
453    return {
454      ua: decodeURIComponent(attribution.ua),
455    };
456  }
457  return null;
458}
459
460const RULES = [
461  {
462    description: "Proton Default AW content",
463    getDefaults(featureConfig) {
464      if (featureConfig?.isProton) {
465        return DEFAULT_PROTON_WELCOME_CONTENT;
466      }
467      return null;
468    },
469  },
470  {
471    description: "Windows pin to task bar screen",
472    getDefaults(featureConfig) {
473      if (featureConfig.needPin) {
474        return {
475          template: "multistage",
476          screens: [
477            {
478              id: "AW_PIN_AND_DEFAULT",
479              order: 0,
480              content: {
481                ...DEFAULT_WELCOME_CONTENT.screens[0].content,
482                title: {
483                  string_id: "onboarding-multistage-pin-default-header",
484                },
485                subtitle: {
486                  string_id: "onboarding-multistage-pin-default-subtitle",
487                },
488                help_text: {
489                  position: "default",
490                  text: {
491                    string_id: "onboarding-multistage-pin-default-help-text",
492                  },
493                },
494                primary_button: {
495                  label: {
496                    string_id:
497                      "onboarding-multistage-pin-default-primary-button-label",
498                  },
499                  action: {
500                    navigate: true,
501                    type: "PIN_AND_DEFAULT",
502                    waitForDefault: true,
503                  },
504                },
505                waiting_for_default: {
506                  subtitle: {
507                    string_id:
508                      "onboarding-multistage-pin-default-waiting-subtitle",
509                  },
510                  help_text: null,
511                  primary_button: null,
512                  tiles: {
513                    media_type: "tiles-delayed",
514                    type: "image",
515                    source: {
516                      default:
517                        "chrome://activity-stream/content/data/content/assets/remote/windows-default-browser.gif",
518                    },
519                  },
520                },
521              },
522            },
523            ...DEFAULT_WELCOME_CONTENT.screens.slice(1),
524          ],
525        };
526      }
527
528      return null;
529    },
530  },
531  {
532    description: "Default AW content",
533    getDefaults() {
534      return DEFAULT_WELCOME_CONTENT;
535    },
536  },
537];
538
539function getDefaults(featureConfig) {
540  for (const rule of RULES) {
541    const result = rule.getDefaults(featureConfig);
542    if (result) {
543      // Make a deep copy of the object to avoid editing the original default.
544      return Cu.cloneInto(result, {});
545    }
546  }
547  return null;
548}
549
550let gSourceL10n = null;
551
552// Localize Firefox download source from user agent attribution to show inside
553// import primary button label such as 'Import from <localized browser name>'.
554// no firefox as import wizard doesn't show it
555const allowedUAs = ["chrome", "edge", "ie"];
556function getLocalizedUA(ua) {
557  if (!gSourceL10n) {
558    gSourceL10n = new Localization(["browser/migration.ftl"]);
559  }
560  if (allowedUAs.includes(ua)) {
561    return gSourceL10n.formatValue(`source-name-${ua.toLowerCase()}`);
562  }
563  return null;
564}
565
566async function prepareContentForReact(content) {
567  if (content?.template === "return_to_amo") {
568    return content;
569  }
570
571  if (content.isProton) {
572    content.design = "proton";
573  }
574
575  // Helper to find screens to remove and adjust screen order.
576  function removeScreens(check) {
577    const { screens } = content;
578    let removed = 0;
579    for (let i = 0; i < screens?.length; i++) {
580      if (check(screens[i])) {
581        screens.splice(i--, 1);
582        removed++;
583      } else if (screens[i].order) {
584        screens[i].order -= removed;
585      }
586    }
587  }
588
589  // Change content for Windows 7 because non-light themes aren't quite right.
590  if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
591    removeScreens(screen => screen.content?.tiles?.type === "theme");
592  }
593
594  // Set the primary import button source based on attribution.
595  if (content?.ua) {
596    // If available, add the browser source to action data
597    // and localized browser string args to primary button label
598    const { label, action } =
599      content?.screens?.find(
600        screen =>
601          screen?.content?.primary_button?.action?.type ===
602          "SHOW_MIGRATION_WIZARD"
603      )?.content?.primary_button ?? {};
604
605    if (action) {
606      action.data = { ...action.data, source: content.ua };
607    }
608
609    let browserStr = await getLocalizedUA(content.ua);
610
611    if (label?.string_id) {
612      label.string_id = browserStr
613        ? "mr1-onboarding-import-primary-button-label-attribution"
614        : "mr1-onboarding-import-primary-button-label-no-attribution";
615
616      label.args = browserStr ? { previous: browserStr } : {};
617    }
618  }
619
620  // If already pinned, convert "pin" screen to "welcome" with desired action.
621  let removeDefault = !content.needDefault;
622  if (!content.needPin) {
623    const pinScreen = content.screens?.find(screen =>
624      screen.id?.startsWith("AW_PIN_FIREFOX")
625    );
626    if (pinScreen?.content) {
627      pinScreen.id = removeDefault ? "AW_GET_STARTED" : "AW_ONLY_DEFAULT";
628      pinScreen.content.title = {
629        string_id: "mr1-onboarding-welcome-header",
630      };
631      pinScreen.content.primary_button = {
632        label: {
633          string_id: removeDefault
634            ? "mr1-onboarding-get-started-primary-button-label"
635            : "mr1-onboarding-set-default-only-primary-button-label",
636        },
637        action: {
638          navigate: true,
639        },
640      };
641
642      // Get started content will navigate without action, so remove "Not now."
643      if (removeDefault) {
644        delete pinScreen.content.secondary_button;
645      } else {
646        // The "pin" screen will now handle "default" so remove other "default."
647        pinScreen.content.primary_button.action.type = "SET_DEFAULT_BROWSER";
648        removeDefault = true;
649      }
650    }
651  }
652  if (removeDefault) {
653    removeScreens(screen => screen.id?.startsWith("AW_SET_DEFAULT"));
654  }
655
656  // Remove Firefox Accounts related UI and prevent related metrics.
657  if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled", false)) {
658    delete content.screens?.find(
659      screen =>
660        screen.content?.secondary_button_top?.action?.type ===
661        "SHOW_FIREFOX_ACCOUNTS"
662    )?.content.secondary_button_top;
663    content.skipFxA = true;
664  }
665
666  // Remove the English-only image caption.
667  if (Services.locale.appLocaleAsBCP47.split("-")[0] !== "en") {
668    delete content.screens?.find(
669      screen => screen.content?.help_text?.deleteIfNotEn
670    )?.content.help_text.text;
671  }
672
673  return content;
674}
675
676const AboutWelcomeDefaults = {
677  prepareContentForReact,
678  getDefaults,
679  getAttributionContent,
680};
681