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