From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- browser/components/newtab/.eslintrc.js | 211 + browser/components/newtab/.nvmrc | 1 + browser/components/newtab/AboutNewTabService.jsm | 523 + .../newtab/aboutwelcome/AboutWelcomeChild.jsm | 339 + .../newtab/aboutwelcome/AboutWelcomeParent.jsm | 266 + .../aboutwelcome/content/aboutwelcome.bundle.js | 2321 ++ .../newtab/aboutwelcome/content/aboutwelcome.css | 1644 ++ .../newtab/aboutwelcome/content/aboutwelcome.html | 49 + .../aboutwelcome/lib/AboutWelcomeDefaults.jsm | 707 + .../aboutwelcome/lib/AboutWelcomeTelemetry.jsm | 242 + .../components/newtab/actors/ASRouterChild.sys.mjs | 111 + .../newtab/actors/ASRouterParent.sys.mjs | 103 + .../newtab/bin/render-activity-stream-html.js | 146 + browser/components/newtab/bin/try-runner.js | 193 + browser/components/newtab/bin/vendor.js | 38 + browser/components/newtab/common/Actions.sys.mjs | 459 + .../newtab/common/ActorConstants.sys.mjs | 47 + browser/components/newtab/common/Dedupe.sys.mjs | 36 + browser/components/newtab/common/Reducers.sys.mjs | 864 + browser/components/newtab/components.conf | 14 + .../newtab/components/CustomElements/paragraph.js | 72 + browser/components/newtab/content-src/.eslintrc.js | 12 + .../content-src/aboutwelcome/aboutwelcome.jsx | 140 + .../content-src/aboutwelcome/aboutwelcome.scss | 1676 ++ .../aboutwelcome/components/AdditionalCTA.jsx | 30 + .../aboutwelcome/components/CTAParagraph.jsx | 45 + .../components/EmbeddedMigrationWizard.jsx | 38 + .../aboutwelcome/components/HelpText.jsx | 49 + .../aboutwelcome/components/HeroImage.jsx | 24 + .../aboutwelcome/components/LanguageSwitcher.jsx | 294 + .../aboutwelcome/components/MRColorways.jsx | 198 + .../aboutwelcome/components/MSLocalized.jsx | 108 + .../aboutwelcome/components/MobileDownloads.jsx | 71 + .../aboutwelcome/components/MultiSelect.jsx | 52 + .../components/MultiStageAboutWelcome.jsx | 468 + .../components/MultiStageProtonScreen.jsx | 472 + .../aboutwelcome/components/OnboardingVideo.jsx | 34 + .../aboutwelcome/components/ReturnToAMO.jsx | 105 + .../content-src/aboutwelcome/components/Themes.jsx | 51 + .../content-src/aboutwelcome/components/Zap.jsx | 60 + .../newtab/content-src/activity-stream.jsx | 57 + .../newtab/content-src/asrouter/README.md | 34 + .../content-src/asrouter/asrouter-content.jsx | 346 + .../newtab/content-src/asrouter/asrouter-utils.js | 108 + .../asrouter/components/Button/Button.jsx | 32 + .../asrouter/components/Button/_Button.scss | 51 + .../ConditionalWrapper/ConditionalWrapper.jsx | 9 + .../ImpressionsWrapper/ImpressionsWrapper.jsx | 76 + .../components/ModalOverlay/ModalOverlay.jsx | 56 + .../components/ModalOverlay/_ModalOverlay.scss | 103 + .../asrouter/components/RichText/RichText.jsx | 86 + .../components/SnippetBase/SnippetBase.jsx | 121 + .../components/SnippetBase/_SnippetBase.scss | 117 + .../asrouter/docs/cfr_doorhanger_screenshot.png | Bin 0 -> 257709 bytes .../content-src/asrouter/docs/debugging-docs.md | 63 + .../content-src/asrouter/docs/debugging-guide.png | Bin 0 -> 247644 bytes .../newtab/content-src/asrouter/docs/first-run.md | 68 + .../newtab/content-src/asrouter/docs/index.rst | 104 + .../asrouter/docs/message-routing-overview.png | Bin 0 -> 50250 bytes .../asrouter/docs/simple-cfr-template.rst | 37 + .../asrouter/docs/targeting-attributes.md | 986 + .../content-src/asrouter/docs/targeting-guide.md | 37 + .../asrouter/docs/telemetry-screenshot.png | Bin 0 -> 104954 bytes .../content-src/asrouter/rich-text-strings.js | 44 + .../BackgroundTaskMessagingExperiment.schema.json | 312 + .../asrouter/schemas/FxMSCommon.schema.json | 128 + .../schemas/MessagingExperiment.schema.json | 1640 ++ .../schemas/corpus/ReachExperiments.messages.json | 15 + .../asrouter/schemas/extract-test-corpus.js | 65 + .../content-src/asrouter/schemas/make-schemas.py | 475 + .../content-src/asrouter/schemas/message-format.md | 101 + .../asrouter/schemas/message-group.schema.json | 64 + .../asrouter/schemas/provider-response.schema.json | 67 + .../newtab/content-src/asrouter/template-utils.js | 22 + .../CFR/templates/CFRUrlbarChiclet.schema.json | 66 + .../CFR/templates/ExtensionDoorhanger.schema.json | 320 + .../templates/CFR/templates/InfoBar.schema.json | 89 + .../asrouter/templates/EOYSnippet/EOYSnippet.jsx | 153 + .../templates/EOYSnippet/EOYSnippet.schema.json | 171 + .../asrouter/templates/EOYSnippet/_EOYSnippet.scss | 55 + .../FXASignupSnippet/FXASignupSnippet.jsx | 38 + .../FXASignupSnippet/FXASignupSnippet.schema.json | 196 + .../asrouter/templates/FirstRun/addUtmParams.js | 32 + .../NewsletterSnippet/NewsletterSnippet.jsx | 34 + .../NewsletterSnippet.schema.json | 186 + .../ProtectionsPanelMessage.schema.json | 62 + .../OnboardingMessage/Spotlight.schema.json | 66 + .../ToolbarBadgeMessage.schema.json | 45 + .../OnboardingMessage/UpdateAction.schema.json | 47 + .../OnboardingMessage/WhatsNewMessage.schema.json | 73 + .../PBNewtab/NewtabPromoMessage.schema.json | 153 + .../SendToDeviceSnippet/SendToDeviceSnippet.jsx | 76 + .../SendToDeviceSnippet.schema.json | 243 + .../SendToDeviceSnippet/isEmailOrPhoneNumber.js | 39 + .../SimpleBelowSearchSnippet.jsx | 133 + .../SimpleBelowSearchSnippet.schema.json | 114 + .../_SimpleBelowSearchSnippet.scss | 190 + .../templates/SimpleSnippet/SimpleSnippet.jsx | 222 + .../SimpleSnippet/SimpleSnippet.schema.json | 159 + .../templates/SimpleSnippet/_SimpleSnippet.scss | 131 + .../SubmitFormScene2Snippet.schema.json | 167 + .../SubmitFormSnippet/SubmitFormSnippet.jsx | 408 + .../SubmitFormSnippet.schema.json | 235 + .../SubmitFormSnippet/_SubmitFormSnippet.scss | 176 + .../ToastNotification.schema.json | 85 + .../asrouter/templates/template-manifest.jsx | 24 + .../components/A11yLinkButton/A11yLinkButton.jsx | 18 + .../components/A11yLinkButton/_A11yLinkButton.scss | 13 + .../components/ASRouterAdmin/ASRouterAdmin.jsx | 1967 ++ .../components/ASRouterAdmin/ASRouterAdmin.scss | 280 + .../components/ASRouterAdmin/CopyButton.jsx | 31 + .../components/ASRouterAdmin/SimpleHashRouter.jsx | 35 + .../newtab/content-src/components/Base/Base.jsx | 273 + .../newtab/content-src/components/Base/_Base.scss | 143 + .../newtab/content-src/components/Card/Card.jsx | 362 + .../newtab/content-src/components/Card/_Card.scss | 331 + .../newtab/content-src/components/Card/types.js | 30 + .../CollapsibleSection/CollapsibleSection.jsx | 116 + .../CollapsibleSection/_CollapsibleSection.scss | 106 + .../ComponentPerfTimer/ComponentPerfTimer.jsx | 177 + .../components/ConfirmDialog/ConfirmDialog.jsx | 103 + .../components/ConfirmDialog/_ConfirmDialog.scss | 68 + .../components/ContextMenu/ContextMenu.jsx | 176 + .../components/ContextMenu/ContextMenuButton.jsx | 72 + .../components/ContextMenu/_ContextMenu.scss | 57 + .../BackgroundsSection/BackgroundsSection.jsx | 11 + .../ContentSection/ContentSection.jsx | 308 + .../components/CustomizeMenu/CustomizeMenu.jsx | 87 + .../components/CustomizeMenu/_CustomizeMenu.scss | 311 + .../DiscoveryStreamBase/DiscoveryStreamBase.jsx | 384 + .../DiscoveryStreamBase/_DiscoveryStreamBase.scss | 67 + .../CardGrid/CardGrid.jsx | 536 + .../CardGrid/_CardGrid.scss | 355 + .../CollectionCardGrid/CollectionCardGrid.jsx | 139 + .../CollectionCardGrid/_CollectionCardGrid.scss | 38 + .../DiscoveryStreamComponents/DSCard/DSCard.jsx | 491 + .../DiscoveryStreamComponents/DSCard/_DSCard.scss | 251 + .../DSContextFooter/DSContextFooter.jsx | 118 + .../DSContextFooter/_DSContextFooter.scss | 81 + .../DSDismiss/DSDismiss.jsx | 57 + .../DSDismiss/_DSDismiss.scss | 47 + .../DSEmptyState/DSEmptyState.jsx | 100 + .../DSEmptyState/_DSEmptyState.scss | 83 + .../DiscoveryStreamComponents/DSImage/DSImage.jsx | 263 + .../DSImage/_DSImage.scss | 48 + .../DSLinkMenu/DSLinkMenu.jsx | 70 + .../DSLinkMenu/_DSLinkMenu.scss | 28 + .../DSMessage/DSMessage.jsx | 34 + .../DSMessage/_DSMessage.scss | 37 + .../DSPrivacyModal/DSPrivacyModal.jsx | 72 + .../DSPrivacyModal/_DSPrivacyModal.scss | 48 + .../DSSignup/DSSignup.jsx | 168 + .../DSSignup/DSSignup.scss | 52 + .../DSTextPromo/DSTextPromo.jsx | 143 + .../DSTextPromo/_DSTextPromo.scss | 92 + .../Highlights/Highlights.jsx | 26 + .../Highlights/_Highlights.scss | 45 + .../HorizontalRule/HorizontalRule.jsx | 11 + .../HorizontalRule/_HorizontalRule.scss | 7 + .../Navigation/Navigation.jsx | 112 + .../Navigation/_Navigation.scss | 180 + .../PrivacyLink/PrivacyLink.jsx | 20 + .../PrivacyLink/_PrivacyLink.scss | 10 + .../SafeAnchor/SafeAnchor.jsx | 65 + .../SectionTitle/SectionTitle.jsx | 19 + .../SectionTitle/_SectionTitle.scss | 18 + .../TopSites/_TopSites.scss | 77 + .../TopicsWidget/TopicsWidget.jsx | 125 + .../TopicsWidget/_TopicsWidget.scss | 88 + .../ImpressionStats.jsx | 250 + .../_ImpressionStats.scss | 7 + .../components/ErrorBoundary/ErrorBoundary.jsx | 68 + .../components/ErrorBoundary/_ErrorBoundary.scss | 21 + .../components/FluentOrText/FluentOrText.jsx | 36 + .../content-src/components/LinkMenu/LinkMenu.jsx | 110 + .../MoreRecommendations/MoreRecommendations.jsx | 21 + .../MoreRecommendations/_MoreRecommendations.scss | 24 + .../PocketLoggedInCta/PocketLoggedInCta.jsx | 42 + .../PocketLoggedInCta/_PocketLoggedInCta.scss | 42 + .../content-src/components/Search/Search.jsx | 223 + .../content-src/components/Search/_Search.scss | 412 + .../content-src/components/Sections/Sections.jsx | 378 + .../content-src/components/Sections/_Sections.scss | 123 + .../components/TopSites/SearchShortcutsForm.jsx | 192 + .../content-src/components/TopSites/TopSite.jsx | 873 + .../components/TopSites/TopSiteForm.jsx | 323 + .../components/TopSites/TopSiteFormInput.jsx | 111 + .../TopSites/TopSiteImpressionWrapper.jsx | 149 + .../content-src/components/TopSites/TopSites.jsx | 213 + .../components/TopSites/TopSitesConstants.js | 39 + .../content-src/components/TopSites/_TopSites.scss | 628 + .../content-src/components/Topics/Topics.jsx | 33 + .../content-src/components/Topics/_Topics.scss | 24 + .../newtab/content-src/lib/aboutwelcome-utils.js | 119 + .../components/newtab/content-src/lib/constants.js | 38 + .../content-src/lib/detect-user-session-start.js | 82 + .../newtab/content-src/lib/init-store.js | 175 + .../newtab/content-src/lib/link-menu-options.js | 292 + .../newtab/content-src/lib/perf-service.js | 104 + .../newtab/content-src/lib/screenshot-utils.js | 61 + .../newtab/content-src/lib/selectLayoutRender.js | 255 + .../content-src/styles/_activity-stream.scss | 177 + .../content-src/styles/_feature-callout-theme.scss | 56 + .../content-src/styles/_feature-callout.scss | 314 + .../newtab/content-src/styles/_icons.scss | 211 + .../newtab/content-src/styles/_mixins.scss | 50 + .../newtab/content-src/styles/_normalize.scss | 29 + .../newtab/content-src/styles/_theme.scss | 99 + .../newtab/content-src/styles/_variables.scss | 217 + .../content-src/styles/activity-stream-linux.scss | 11 + .../content-src/styles/activity-stream-mac.scss | 16 + .../styles/activity-stream-windows.scss | 11 + .../newtab/css/activity-stream-linux.css | 4812 ++++ .../components/newtab/css/activity-stream-mac.css | 4816 ++++ .../newtab/css/activity-stream-windows.css | 4812 ++++ .../data/content/abouthomecache/page.html.template | 44 + .../data/content/abouthomecache/script.js.template | 19 + .../newtab/data/content/activity-stream.bundle.js | 15290 +++++++++++ .../newtab/data/content/assets/confetti.svg | 55 + .../newtab/data/content/assets/default.svg | 6 + .../data/content/assets/device-migration.svg | 36 + .../newtab/data/content/assets/firefox.svg | 168 + .../data/content/assets/glyph-cfr-feature-16.svg | 4 + .../newtab/data/content/assets/glyph-mail-16.svg | 4 + .../data/content/assets/glyph-maximize-16.svg | 4 + .../data/content/assets/glyph-minimize-16.svg | 4 + .../data/content/assets/glyph-modal-delete-20.svg | 8 + .../data/content/assets/glyph-newWindow-16.svg | 4 + .../data/content/assets/glyph-open-file-16.svg | 4 + .../newtab/data/content/assets/glyph-pin-16.svg | 6 + .../content/assets/glyph-pocket-archive-16.svg | 4 + .../data/content/assets/glyph-pocket-delete-16.svg | 4 + .../newtab/data/content/assets/glyph-unpin-16.svg | 4 + .../data/content/assets/glyph-webextension-16.svg | 4 + .../newtab/data/content/assets/heart.webp | Bin 0 -> 100396 bytes .../data/content/assets/icon-removed-bookmark.svg | 4 + .../newtab/data/content/assets/long-zap.svg | 4 + .../assets/mobile-download-qr-existing-user-cn.svg | 5 + .../assets/mobile-download-qr-existing-user.svg | 7 + .../assets/mobile-download-qr-new-user-cn.svg | 4 + .../content/assets/mobile-download-qr-new-user.svg | 7 + .../newtab/data/content/assets/mr-colorways.avif | Bin 0 -> 63824 bytes .../newtab/data/content/assets/mr-gratitude.svg | 4 + .../newtab/data/content/assets/mr-import.svg | 4 + .../data/content/assets/mr-mobilecrosspromo.svg | 4 + .../newtab/data/content/assets/mr-pinprivate.svg | 4 + .../newtab/data/content/assets/mr-pintaskbar.svg | 4 + .../data/content/assets/mr-privacysegmentation.svg | 4 + .../content/assets/mr-rtamo-background-image.svg | 4 + .../newtab/data/content/assets/mr-settodefault.svg | 4 + .../newtab/data/content/assets/noodle-C.svg | 6 + .../data/content/assets/noodle-outline-L.svg | 13 + .../newtab/data/content/assets/noodle-solid-L.svg | 6 + .../newtab/data/content/assets/person-typing.svg | 4 + .../data/content/assets/pocket-onboarding.avif | Bin 0 -> 7462 bytes .../data/content/assets/pocket-onboarding@2x.avif | Bin 0 -> 18590 bytes .../newtab/data/content/assets/pocket-swoosh.svg | 11 + .../newtab/data/content/assets/remote/mountain.svg | 12 + .../newtab/data/content/assets/remote/umbrella.png | Bin 0 -> 4292 bytes .../newtab/data/content/assets/short-zap.svg | 4 + .../newtab/data/content/assets/spinner.svg | 4 + .../newtab/data/content/newtab-render.js | 11 + .../data/content/tippytop/favicons/adidas.png | Bin 0 -> 3226 bytes .../content/tippytop/favicons/aliexpress-com.ico | Bin 0 -> 4286 bytes .../data/content/tippytop/favicons/allegro-pl.ico | Bin 0 -> 1150 bytes .../data/content/tippytop/favicons/amazon.ico | Bin 0 -> 1407 bytes .../data/content/tippytop/favicons/avito-ru.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/baidu-com.png | Bin 0 -> 1983 bytes .../data/content/tippytop/favicons/bbc-uk.ico | Bin 0 -> 958 bytes .../data/content/tippytop/favicons/bing-com.ico | Bin 0 -> 4286 bytes .../data/content/tippytop/favicons/ctrip-com.ico | Bin 0 -> 1150 bytes .../content/tippytop/favicons/duckduckgo-com.ico | Bin 0 -> 2799 bytes .../newtab/data/content/tippytop/favicons/ebay.ico | Bin 0 -> 1455 bytes .../newtab/data/content/tippytop/favicons/etsy.ico | Bin 0 -> 4286 bytes .../content/tippytop/favicons/facebook-com.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/geico.png | Bin 0 -> 1472 bytes .../data/content/tippytop/favicons/google-com.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/hrblock.ico | Bin 0 -> 3950 bytes .../data/content/tippytop/favicons/ifeng-com.ico | Bin 0 -> 4038 bytes .../data/content/tippytop/favicons/iqiyi-com.ico | Bin 0 -> 5430 bytes .../content/tippytop/favicons/leboncoin-fr.png | Bin 0 -> 454 bytes .../newtab/data/content/tippytop/favicons/nike.ico | Bin 0 -> 1150 bytes .../data/content/tippytop/favicons/ok-ru.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/olx-pl.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/reddit-com.png | Bin 0 -> 2094 bytes .../data/content/tippytop/favicons/samsung.ico | Bin 0 -> 4286 bytes .../data/content/tippytop/favicons/turbotax.png | Bin 0 -> 3744 bytes .../data/content/tippytop/favicons/twitter-com.ico | Bin 0 -> 1650 bytes .../data/content/tippytop/favicons/vk-com.ico | Bin 0 -> 302 bytes .../data/content/tippytop/favicons/vodafone.png | Bin 0 -> 1757 bytes .../data/content/tippytop/favicons/weibo-com.ico | Bin 0 -> 10134 bytes .../content/tippytop/favicons/wikipedia-org.ico | Bin 0 -> 2734 bytes .../newtab/data/content/tippytop/favicons/wix.ico | Bin 0 -> 1061 bytes .../data/content/tippytop/favicons/wykop-pl.png | Bin 0 -> 1705 bytes .../data/content/tippytop/favicons/yandex-com.png | Bin 0 -> 1338 bytes .../data/content/tippytop/favicons/yandex-ru.png | Bin 0 -> 1368 bytes .../data/content/tippytop/favicons/youtube-com.png | Bin 0 -> 348 bytes .../data/content/tippytop/favicons/zhihu-com.ico | Bin 0 -> 6518 bytes .../data/content/tippytop/images/adidas@2x.png | Bin 0 -> 5448 bytes .../content/tippytop/images/aliexpress-com@2x.png | Bin 0 -> 12459 bytes .../data/content/tippytop/images/allegro-pl@2x.png | Bin 0 -> 5041 bytes .../data/content/tippytop/images/amazon@2x.png | Bin 0 -> 6061 bytes .../data/content/tippytop/images/avito-ru@2x.png | Bin 0 -> 1568 bytes .../data/content/tippytop/images/baidu-com@2x.png | Bin 0 -> 8198 bytes .../data/content/tippytop/images/bbc-uk@2x.png | Bin 0 -> 18207 bytes .../data/content/tippytop/images/bing-com@2x.svg | 106 + .../data/content/tippytop/images/ctrip-com@2x.png | Bin 0 -> 15862 bytes .../content/tippytop/images/duckduckgo-com@2x.svg | 12 + .../data/content/tippytop/images/ebay@2x.png | Bin 0 -> 5361 bytes .../data/content/tippytop/images/etsy@2x.jpg | Bin 0 -> 4094 bytes .../content/tippytop/images/facebook-com@2x.png | Bin 0 -> 10780 bytes .../data/content/tippytop/images/geico@2x.jpg | Bin 0 -> 11834 bytes .../data/content/tippytop/images/google-com@2x.png | Bin 0 -> 3035 bytes .../data/content/tippytop/images/hrblock@2x.png | Bin 0 -> 4642 bytes .../data/content/tippytop/images/ifeng-com@2x.png | Bin 0 -> 22282 bytes .../data/content/tippytop/images/iqiyi-com@2x.png | Bin 0 -> 14340 bytes .../content/tippytop/images/leboncoin-fr@2x.png | Bin 0 -> 7146 bytes .../data/content/tippytop/images/nike@2x.jpg | Bin 0 -> 5163 bytes .../data/content/tippytop/images/ok-ru@2x.png | Bin 0 -> 2526 bytes .../data/content/tippytop/images/olx-pl@2x.png | Bin 0 -> 5287 bytes .../data/content/tippytop/images/reddit-com@2x.png | Bin 0 -> 5180 bytes .../data/content/tippytop/images/samsung@2x.jpg | Bin 0 -> 3347 bytes .../data/content/tippytop/images/turbotax@2x.jpg | Bin 0 -> 11930 bytes .../content/tippytop/images/twitter-com@2x.png | Bin 0 -> 1260 bytes .../data/content/tippytop/images/vk-com@2x.png | Bin 0 -> 9897 bytes .../data/content/tippytop/images/vodafone@2x.jpg | Bin 0 -> 7050 bytes .../data/content/tippytop/images/weibo-com@2x.png | Bin 0 -> 15507 bytes .../content/tippytop/images/wikipedia-org@2x.png | Bin 0 -> 19001 bytes .../newtab/data/content/tippytop/images/wix@2x.jpg | Bin 0 -> 8714 bytes .../data/content/tippytop/images/wykop-pl@2x.png | Bin 0 -> 4415 bytes .../data/content/tippytop/images/yandex-com@2x.png | Bin 0 -> 6516 bytes .../data/content/tippytop/images/yandex-ru@2x.png | Bin 0 -> 6638 bytes .../content/tippytop/images/youtube-com@2x.png | Bin 0 -> 2924 bytes .../data/content/tippytop/images/zhihu-com@2x.png | Bin 0 -> 10225 bytes .../newtab/data/content/tippytop/top_sites.json | 182 + browser/components/newtab/docs/index.rst | 110 + .../v2-system-addon/about_home_startup_cache.md | 86 + .../newtab/docs/v2-system-addon/data_dictionary.md | 360 + .../newtab/docs/v2-system-addon/data_events.md | 1417 ++ .../newtab/docs/v2-system-addon/geo_locale.md | 23 + .../newtab/docs/v2-system-addon/mochitests.md | 28 + .../newtab/docs/v2-system-addon/preferences.md | 278 + .../newtab/docs/v2-system-addon/remote_cfr.md | 82 + .../newtab/docs/v2-system-addon/sections.md | 82 + .../newtab/docs/v2-system-addon/telemetry.md | 13 + .../newtab/docs/v2-system-addon/tippytop.md | 40 + .../docs/v2-system-addon/unit_testing_guide.md | 149 + browser/components/newtab/jar.mn | 46 + browser/components/newtab/karma.mc.config.js | 346 + browser/components/newtab/lib/ASRouter.jsm | 2101 ++ .../newtab/lib/ASRouterDefaultConfig.jsm | 65 + .../newtab/lib/ASRouterNewTabHook.sys.mjs | 117 + .../lib/ASRouterParentProcessMessageHandler.jsm | 179 + .../components/newtab/lib/ASRouterPreferences.jsm | 259 + .../components/newtab/lib/ASRouterTargeting.jsm | 1225 + .../newtab/lib/ASRouterTriggerListeners.jsm | 1087 + browser/components/newtab/lib/AWScreenUtils.jsm | 79 + browser/components/newtab/lib/AboutPreferences.jsm | 312 + browser/components/newtab/lib/ActivityStream.jsm | 758 + .../newtab/lib/ActivityStreamMessageChannel.jsm | 332 + .../components/newtab/lib/ActivityStreamPrefs.jsm | 95 + .../newtab/lib/ActivityStreamStorage.jsm | 121 + .../newtab/lib/CFRMessageProvider.sys.mjs | 821 + browser/components/newtab/lib/CFRPageActions.jsm | 1049 + browser/components/newtab/lib/DefaultSites.sys.mjs | 46 + .../components/newtab/lib/DiscoveryStreamFeed.jsm | 2427 ++ browser/components/newtab/lib/DownloadsManager.jsm | 191 + browser/components/newtab/lib/FaviconFeed.jsm | 197 + .../newtab/lib/FeatureCalloutMessages.sys.mjs | 648 + browser/components/newtab/lib/FilterAdult.jsm | 3036 +++ browser/components/newtab/lib/HighlightsFeed.jsm | 350 + browser/components/newtab/lib/InfoBar.jsm | 172 + browser/components/newtab/lib/LinksCache.sys.mjs | 133 + browser/components/newtab/lib/MomentsPageHub.jsm | 174 + browser/components/newtab/lib/NewTabInit.jsm | 57 + .../newtab/lib/OnboardingMessageProvider.jsm | 1276 + .../components/newtab/lib/PageEventManager.sys.mjs | 94 + .../newtab/lib/PanelTestProvider.sys.mjs | 626 + .../components/newtab/lib/PersistentCache.sys.mjs | 92 + .../PersonalityProvider/NaiveBayesTextTagger.jsm | 67 + .../lib/PersonalityProvider/NmfTextTagger.jsm | 65 + .../PersonalityProvider/PersonalityProvider.jsm | 282 + .../PersonalityProviderWorker.js | 44 + .../PersonalityProviderWorkerClass.jsm | 311 + .../lib/PersonalityProvider/RecipeExecutor.jsm | 1126 + .../newtab/lib/PersonalityProvider/Tokenize.jsm | 89 + browser/components/newtab/lib/PlacesFeed.jsm | 566 + browser/components/newtab/lib/PrefsFeed.jsm | 280 + .../newtab/lib/RecommendationProvider.jsm | 132 + browser/components/newtab/lib/RemoteL10n.sys.mjs | 249 + browser/components/newtab/lib/Screenshots.jsm | 136 + .../components/newtab/lib/SearchShortcuts.sys.mjs | 67 + browser/components/newtab/lib/SectionsManager.jsm | 712 + browser/components/newtab/lib/ShortURL.jsm | 83 + browser/components/newtab/lib/SiteClassifier.jsm | 99 + .../newtab/lib/SnippetsTestMessageProvider.sys.mjs | 688 + browser/components/newtab/lib/Spotlight.jsm | 85 + browser/components/newtab/lib/Store.jsm | 195 + browser/components/newtab/lib/SystemTickFeed.jsm | 75 + browser/components/newtab/lib/TelemetryFeed.jsm | 1403 ++ .../components/newtab/lib/TippyTopProvider.sys.mjs | 60 + .../components/newtab/lib/ToastNotification.jsm | 118 + browser/components/newtab/lib/ToolbarBadgeHub.jsm | 318 + browser/components/newtab/lib/ToolbarPanelHub.jsm | 612 + browser/components/newtab/lib/TopSitesFeed.jsm | 1715 ++ browser/components/newtab/lib/TopStoriesFeed.jsm | 746 + .../components/newtab/lib/UTEventReporting.sys.mjs | 62 + browser/components/newtab/lib/cache-worker.js | 205 + browser/components/newtab/loaders/inject-loader.js | 59 + browser/components/newtab/metrics.yaml | 1221 + browser/components/newtab/moz.build | 58 + .../components/newtab/nsIAboutNewTabService.idl | 39 + browser/components/newtab/package-lock.json | 12021 +++++++++ browser/components/newtab/package.json | 124 + browser/components/newtab/pings.yaml | 45 + .../newtab/prerendered/activity-stream-debug.html | 47 + .../prerendered/activity-stream-noscripts.html | 36 + .../newtab/prerendered/activity-stream.html | 47 + browser/components/newtab/test/.eslintrc.js | 41 + .../newtab/test/InflightAssetsMessageProvider.jsm | 342 + .../newtab/test/browser/abouthomecache/browser.ini | 39 + .../abouthomecache/browser_basic_endtoend.js | 22 + .../browser/abouthomecache/browser_bump_version.js | 35 + .../browser/abouthomecache/browser_disabled.js | 97 + .../browser_experiments_api_control.js | 63 + .../abouthomecache/browser_locale_change.js | 30 + .../browser/abouthomecache/browser_no_cache.js | 27 + .../browser_no_cache_on_SessionStartup_restore.js | 37 + .../abouthomecache/browser_no_startup_actions.js | 83 + .../abouthomecache/browser_overwrite_cache.js | 38 + .../abouthomecache/browser_process_crash.js | 81 + .../abouthomecache/browser_same_consumer.js | 52 + .../browser/abouthomecache/browser_sanitize.js | 54 + .../abouthomecache/browser_shutdown_timeout.js | 45 + .../newtab/test/browser/abouthomecache/head.js | 360 + .../newtab/test/browser/annotation_first.html | 2 + .../newtab/test/browser/annotation_second.html | 2 + .../newtab/test/browser/annotation_third.html | 2 + .../components/newtab/test/browser/blue_page.html | 6 + browser/components/newtab/test/browser/browser.ini | 112 + .../browser/browser_aboutwelcome_attribution.js | 214 + .../browser_aboutwelcome_configurable_ui.js | 668 + .../browser_aboutwelcome_fxa_signin_flow.js | 303 + .../test/browser/browser_aboutwelcome_glean.js | 174 + .../test/browser/browser_aboutwelcome_import.js | 106 + .../browser_aboutwelcome_mobile_downloads.js | 112 + .../browser_aboutwelcome_multistage_default.js | 736 + ...rowser_aboutwelcome_multistage_experimentAPI.js | 597 + ...ser_aboutwelcome_multistage_languageSwitcher.js | 705 + .../browser/browser_aboutwelcome_multistage_mr.js | 621 + .../browser_aboutwelcome_multistage_video.js | 97 + .../test/browser/browser_aboutwelcome_observer.js | 71 + .../test/browser/browser_aboutwelcome_rtamo.js | 298 + .../browser_aboutwelcome_screen_targeting.js | 152 + .../browser_aboutwelcome_upgrade_multistage_mr.js | 316 + .../test/browser/browser_as_load_location.js | 44 + .../newtab/test/browser/browser_as_render.js | 83 + .../test/browser/browser_asrouter_bug1761522.js | 232 + .../test/browser/browser_asrouter_bug1800087.js | 48 + .../newtab/test/browser/browser_asrouter_cfr.js | 914 + .../browser_asrouter_experimentsAPILoader.js | 505 + .../browser/browser_asrouter_group_frequency.js | 190 + .../browser/browser_asrouter_group_userprefs.js | 160 + .../test/browser/browser_asrouter_infobar.js | 226 + .../browser/browser_asrouter_momentspagehub.js | 116 + .../test/browser/browser_asrouter_snippets.js | 190 + .../browser/browser_asrouter_snippets_dismiss.js | 99 + .../test/browser/browser_asrouter_targeting.js | 1697 ++ .../browser/browser_asrouter_toast_notification.js | 139 + .../test/browser/browser_asrouter_toolbarbadge.js | 149 + .../test/browser/browser_context_menu_item.js | 18 + .../test/browser/browser_customize_menu_content.js | 222 + .../test/browser/browser_customize_menu_render.js | 27 + .../newtab/test/browser/browser_discovery_card.js | 44 + .../test/browser/browser_discovery_render.js | 32 + .../test/browser/browser_discovery_styles.js | 171 + .../test/browser/browser_enabled_newtabpage.js | 33 + .../browser/browser_feature_callout_in_chrome.js | 487 + .../newtab/test/browser/browser_getScreenshots.js | 90 + .../test/browser/browser_highlights_section.js | 96 + .../test/browser/browser_multistage_spotlight.js | 58 + .../browser_multistage_spotlight_telemetry.js | 145 + .../newtab/test/browser/browser_newtab_header.js | 76 + .../test/browser/browser_newtab_last_LinkMenu.js | 151 + .../test/browser/browser_newtab_overrides.js | 138 + .../newtab/test/browser/browser_newtab_ping.js | 216 + .../newtab/test/browser/browser_newtab_towindow.js | 45 + .../newtab/test/browser/browser_newtab_trigger.js | 50 + .../newtab/test/browser/browser_open_tab_focus.js | 37 + .../newtab/test/browser/browser_remote_l10n.js | 56 + .../test/browser/browser_topsites_annotation.js | 980 + .../browser_topsites_contextMenu_options.js | 126 + .../test/browser/browser_topsites_section.js | 299 + .../test/browser/browser_trigger_listeners.js | 343 + .../test/browser/browser_trigger_messagesLoaded.js | 152 + .../components/newtab/test/browser/ds_layout.json | 90 + .../components/newtab/test/browser/file_pdf.PDF | 12 + browser/components/newtab/test/browser/head.js | 392 + .../components/newtab/test/browser/red_page.html | 6 + .../components/newtab/test/browser/redirect_to.sjs | 9 + .../components/newtab/test/browser/snippet.json | 46 + .../test/browser/snippet_below_search_test.json | 20 + .../newtab/test/browser/snippet_simple_test.json | 24 + .../components/newtab/test/browser/topstories.json | 53 + browser/components/newtab/test/schemas/pings.js | 304 + .../test/unit/aboutwelcome/AWScreenUtils.test.jsx | 140 + .../test/unit/aboutwelcome/CTAParagraph.test.jsx | 49 + .../test/unit/aboutwelcome/HeroImage.test.jsx | 40 + .../test/unit/aboutwelcome/MRColorways.test.jsx | 328 + .../unit/aboutwelcome/MobileDownloads.test.jsx | 69 + .../test/unit/aboutwelcome/MultiSelect.test.jsx | 151 + .../unit/aboutwelcome/MultiStageAWProton.test.jsx | 564 + .../aboutwelcome/MultiStageAboutWelcome.test.jsx | 824 + .../unit/aboutwelcome/OnboardingVideoTest.test.jsx | 45 + .../newtab/test/unit/asrouter/ASRouter.test.js | 3040 +++ .../test/unit/asrouter/ASRouterChild.test.js | 74 + .../test/unit/asrouter/ASRouterNewTabHook.test.js | 153 + .../test/unit/asrouter/ASRouterParent.test.js | 106 + .../ASRouterParentProcessMessageHandler.test.js | 428 + .../test/unit/asrouter/ASRouterPreferences.test.js | 491 + .../test/unit/asrouter/ASRouterTargeting.test.js | 574 + .../unit/asrouter/ASRouterTriggerListeners.test.js | 778 + .../test/unit/asrouter/CFRMessageProvider.test.js | 32 + .../test/unit/asrouter/CFRPageActions.test.js | 1252 + .../test/unit/asrouter/MessageLoaderUtils.test.js | 459 + .../test/unit/asrouter/ModalOverlay.test.jsx | 69 + .../newtab/test/unit/asrouter/RemoteL10n.test.js | 217 + .../newtab/test/unit/asrouter/RichText.test.jsx | 101 + .../asrouter/SnippetsTestMessageProvider.test.js | 43 + .../test/unit/asrouter/TargetingDocs.test.js | 88 + .../test/unit/asrouter/asrouter-content.test.jsx | 516 + .../test/unit/asrouter/asrouter-utils.test.js | 100 + .../compatibility-reference/fx57-compat.test.js | 26 + .../compatibility-reference/snippets-fx57.js | 125 + .../newtab/test/unit/asrouter/constants.js | 137 + .../test/unit/asrouter/template-utils.test.js | 31 + .../unit/asrouter/templates/EOYSnippet.test.jsx | 213 + .../templates/ExtensionDoorhanger.test.jsx | 112 + .../asrouter/templates/FXASignupSnippet.test.jsx | 106 + .../asrouter/templates/NewsletterSnippet.test.jsx | 108 + .../templates/SendToDeviceSnippet.test.jsx | 277 + .../templates/SimpleBelowSearchSnippet.test.jsx | 81 + .../unit/asrouter/templates/SimpleSnippet.test.jsx | 259 + .../asrouter/templates/SubmitFormSnippet.test.jsx | 354 + .../templates/isEmailOrPhoneNumber.test.js | 56 + .../newtab/test/unit/common/Actions.test.js | 236 + .../newtab/test/unit/common/Dedupe.test.js | 38 + .../newtab/test/unit/common/Reducers.test.js | 1566 ++ .../content-src/components/ASRouterAdmin.test.jsx | 516 + .../test/unit/content-src/components/Base.test.jsx | 130 + .../test/unit/content-src/components/Card.test.jsx | 510 + .../components/CollapsibleSection.test.jsx | 67 + .../components/ComponentPerfTimer.test.jsx | 447 + .../content-src/components/ConfirmDialog.test.jsx | 182 + .../content-src/components/ContextMenu.test.jsx | 227 + .../content-src/components/CustomiseMenu.test.jsx | 72 + .../components/DiscoveryStreamBase.test.jsx | 313 + .../DiscoveryStreamComponents/CardGrid.test.jsx | 354 + .../CollectionCardGrid.test.jsx | 134 + .../DiscoveryStreamComponents/DSCard.test.jsx | 544 + .../DSContextFooter.test.jsx | 138 + .../DiscoveryStreamComponents/DSDismiss.test.jsx | 51 + .../DSEmptyState.test.jsx | 73 + .../DiscoveryStreamComponents/DSImage.test.jsx | 146 + .../DiscoveryStreamComponents/DSLinkMenu.test.jsx | 151 + .../DiscoveryStreamComponents/DSMessage.test.jsx | 57 + .../DSPrivacyModal.test.jsx | 50 + .../DiscoveryStreamComponents/DSSignup.test.jsx | 92 + .../DiscoveryStreamComponents/DSTextPromo.test.jsx | 94 + .../DiscoveryStreamComponents/Highlights.test.jsx | 41 + .../HorizontalRule.test.jsx | 16 + .../ImpressionStats.test.jsx | 278 + .../DiscoveryStreamComponents/Navigation.test.jsx | 131 + .../DiscoveryStreamComponents/PrivacyLink.test.jsx | 29 + .../DiscoveryStreamComponents/SafeAnchor.test.jsx | 56 + .../SectionTitle.test.jsx | 22 + .../TopicsWidget.test.jsx | 238 + .../content-src/components/ErrorBoundary.test.jsx | 110 + .../content-src/components/FluentOrText.test.jsx | 68 + .../unit/content-src/components/HelpText.test.jsx | 41 + .../unit/content-src/components/LinkMenu.test.jsx | 582 + .../content-src/components/MSLocalized.test.jsx | 48 + .../components/MoreRecommendations.test.jsx | 24 + .../components/PocketLoggedInCta.test.jsx | 46 + .../unit/content-src/components/Search.test.jsx | 179 + .../unit/content-src/components/Sections.test.jsx | 600 + .../unit/content-src/components/TopSites.test.jsx | 1919 ++ .../TopSites/SearchShortcutsForm.test.jsx | 56 + .../TopSites/TopSiteImpressionWrapper.test.jsx | 150 + .../unit/content-src/components/Topics.test.jsx | 22 + .../content-src/components/addUtmParams.test.js | 35 + .../lib/detect-user-session-start.test.js | 120 + .../test/unit/content-src/lib/init-store.test.js | 207 + .../test/unit/content-src/lib/perf-service.test.js | 89 + .../unit/content-src/lib/screenshot-utils.test.js | 147 + .../content-src/lib/selectLayoutRender.test.js | 576 + .../newtab/test/unit/lib/AboutPreferences.test.js | 429 + .../newtab/test/unit/lib/ActivityStream.test.js | 576 + .../unit/lib/ActivityStreamMessageChannel.test.js | 432 + .../test/unit/lib/ActivityStreamPrefs.test.js | 113 + .../test/unit/lib/ActivityStreamStorage.test.js | 161 + .../test/unit/lib/DiscoveryStreamFeed.test.js | 3581 +++ .../newtab/test/unit/lib/DownloadsManager.test.js | 373 + .../newtab/test/unit/lib/FaviconFeed.test.js | 233 + .../newtab/test/unit/lib/FilterAdult.test.js | 112 + .../newtab/test/unit/lib/HighlightsFeed.test.js | 822 + .../newtab/test/unit/lib/LinksCache.test.js | 16 + .../newtab/test/unit/lib/MomentsPageHub.test.js | 336 + .../newtab/test/unit/lib/NewTabInit.test.js | 81 + .../newtab/test/unit/lib/PersistentCache.test.js | 142 + .../NaiveBayesTextTagger.test.js | 95 + .../lib/PersonalityProvider/NmfTextTagger.test.js | 479 + .../PersonalityProvider.test.js | 356 + .../PersonalityProviderWorkerClass.test.js | 456 + .../lib/PersonalityProvider/RecipeExecutor.test.js | 1543 ++ .../unit/lib/PersonalityProvider/Tokenize.test.js | 134 + .../newtab/test/unit/lib/PlacesFeed.test.js | 1245 + .../newtab/test/unit/lib/PrefsFeed.test.js | 357 + .../test/unit/lib/RecommendationProvider.test.js | 162 + .../newtab/test/unit/lib/Screenshots.test.js | 209 + .../newtab/test/unit/lib/SectionsManager.test.js | 897 + .../newtab/test/unit/lib/ShortUrl.test.js | 104 + .../newtab/test/unit/lib/SiteClassifier.test.js | 252 + .../components/newtab/test/unit/lib/Store.test.js | 305 + .../newtab/test/unit/lib/SystemTickFeed.test.js | 76 + .../newtab/test/unit/lib/TelemetryFeed.test.js | 2606 ++ .../newtab/test/unit/lib/TippyTopProvider.test.js | 121 + .../newtab/test/unit/lib/ToolbarBadgeHub.test.js | 649 + .../newtab/test/unit/lib/ToolbarPanelHub.test.js | 934 + .../newtab/test/unit/lib/TopSitesFeed.test.js | 3020 +++ .../newtab/test/unit/lib/TopStoriesFeed.test.js | 1903 ++ .../newtab/test/unit/lib/UTEventReporting.test.js | 115 + browser/components/newtab/test/unit/unit-entry.js | 684 + browser/components/newtab/test/unit/utils.js | 406 + .../components/newtab/test/xpcshell/ds_layout.json | 89 + browser/components/newtab/test/xpcshell/head.js | 105 + .../xpcshell/test_ASRouterTargeting_attribution.js | 98 + .../xpcshell/test_ASRouterTargeting_snapshot.js | 138 + .../test_ASRouter_getTargetingParameters.js | 73 + .../xpcshell/test_AboutHomeStartupCacheChild.js | 33 + .../xpcshell/test_AboutHomeStartupCacheWorker.js | 251 + .../newtab/test/xpcshell/test_AboutNewTab.js | 359 + .../test/xpcshell/test_AboutWelcomeAttribution.js | 69 + .../test/xpcshell/test_AboutWelcomeTelemetry.js | 101 + .../xpcshell/test_AboutWelcomeTelemetry_glean.js | 143 + .../test/xpcshell/test_CFRMessageProvider.js | 32 + .../xpcshell/test_InflightAssetsMessageProvider.js | 41 + .../xpcshell/test_OnboardingMessageProvider.js | 229 + .../newtab/test/xpcshell/test_PanelTestProvider.js | 83 + .../newtab/test/xpcshell/test_reach_experiments.js | 97 + .../newtab/test/xpcshell/test_remoteExperiments.js | 37 + .../newtab/test/xpcshell/topstories.json | 53 + .../components/newtab/test/xpcshell/xpcshell.ini | 32 + .../newtab/tools/babel-jsm-to-commonjs.js | 287 + .../components/newtab/tools/resourceUriPlugin.js | 41 + .../components/newtab/vendor/PROP_TYPES_LICENSE | 21 + .../newtab/vendor/REACT_AND_REACT_DOM_LICENSE | 21 + .../components/newtab/vendor/REACT_REDUX_LICENSE | 21 + .../newtab/vendor/REACT_TRANSITION_GROUP_LICENSE | 30 + browser/components/newtab/vendor/REDUX_LICENSE | 21 + browser/components/newtab/vendor/Redux.jsm | 690 + browser/components/newtab/vendor/prop-types.js | 1 + browser/components/newtab/vendor/react-dev.js | 3318 +++ browser/components/newtab/vendor/react-dom-dev.js | 25147 +++++++++++++++++++ .../components/newtab/vendor/react-dom-server.js | 45 + browser/components/newtab/vendor/react-dom.js | 239 + browser/components/newtab/vendor/react-redux.js | 1 + .../newtab/vendor/react-transition-group.js | 9 + browser/components/newtab/vendor/react.js | 32 + browser/components/newtab/vendor/redux.js | 948 + .../newtab/webpack.aboutwelcome.config.js | 24 + .../newtab/webpack.system-addon.config.js | 80 + browser/components/newtab/yamscripts.yml | 58 + 673 files changed, 213102 insertions(+) create mode 100644 browser/components/newtab/.eslintrc.js create mode 100644 browser/components/newtab/.nvmrc create mode 100644 browser/components/newtab/AboutNewTabService.jsm create mode 100644 browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm create mode 100644 browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm create mode 100644 browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js create mode 100644 browser/components/newtab/aboutwelcome/content/aboutwelcome.css create mode 100644 browser/components/newtab/aboutwelcome/content/aboutwelcome.html create mode 100644 browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm create mode 100644 browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm create mode 100644 browser/components/newtab/actors/ASRouterChild.sys.mjs create mode 100644 browser/components/newtab/actors/ASRouterParent.sys.mjs create mode 100644 browser/components/newtab/bin/render-activity-stream-html.js create mode 100644 browser/components/newtab/bin/try-runner.js create mode 100644 browser/components/newtab/bin/vendor.js create mode 100644 browser/components/newtab/common/Actions.sys.mjs create mode 100644 browser/components/newtab/common/ActorConstants.sys.mjs create mode 100644 browser/components/newtab/common/Dedupe.sys.mjs create mode 100644 browser/components/newtab/common/Reducers.sys.mjs create mode 100644 browser/components/newtab/components.conf create mode 100644 browser/components/newtab/components/CustomElements/paragraph.js create mode 100644 browser/components/newtab/content-src/.eslintrc.js create mode 100644 browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/AdditionalCTA.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/CTAParagraph.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/EmbeddedMigrationWizard.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/HelpText.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/HeroImage.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/LanguageSwitcher.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/MRColorways.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/MobileDownloads.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/MultiSelect.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/MultiStageProtonScreen.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/OnboardingVideo.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/Themes.jsx create mode 100644 browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx create mode 100644 browser/components/newtab/content-src/activity-stream.jsx create mode 100644 browser/components/newtab/content-src/asrouter/README.md create mode 100644 browser/components/newtab/content-src/asrouter/asrouter-content.jsx create mode 100644 browser/components/newtab/content-src/asrouter/asrouter-utils.js create mode 100644 browser/components/newtab/content-src/asrouter/components/Button/Button.jsx create mode 100644 browser/components/newtab/content-src/asrouter/components/Button/_Button.scss create mode 100644 browser/components/newtab/content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx create mode 100644 browser/components/newtab/content-src/asrouter/components/ImpressionsWrapper/ImpressionsWrapper.jsx create mode 100644 browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx create mode 100644 browser/components/newtab/content-src/asrouter/components/ModalOverlay/_ModalOverlay.scss create mode 100644 browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx create mode 100644 browser/components/newtab/content-src/asrouter/components/SnippetBase/SnippetBase.jsx create mode 100644 browser/components/newtab/content-src/asrouter/components/SnippetBase/_SnippetBase.scss create mode 100644 browser/components/newtab/content-src/asrouter/docs/cfr_doorhanger_screenshot.png create mode 100644 browser/components/newtab/content-src/asrouter/docs/debugging-docs.md create mode 100644 browser/components/newtab/content-src/asrouter/docs/debugging-guide.png create mode 100644 browser/components/newtab/content-src/asrouter/docs/first-run.md create mode 100644 browser/components/newtab/content-src/asrouter/docs/index.rst create mode 100644 browser/components/newtab/content-src/asrouter/docs/message-routing-overview.png create mode 100644 browser/components/newtab/content-src/asrouter/docs/simple-cfr-template.rst create mode 100644 browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md create mode 100644 browser/components/newtab/content-src/asrouter/docs/targeting-guide.md create mode 100644 browser/components/newtab/content-src/asrouter/docs/telemetry-screenshot.png create mode 100644 browser/components/newtab/content-src/asrouter/rich-text-strings.js create mode 100644 browser/components/newtab/content-src/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/FxMSCommon.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/MessagingExperiment.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/corpus/ReachExperiments.messages.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/extract-test-corpus.js create mode 100755 browser/components/newtab/content-src/asrouter/schemas/make-schemas.py create mode 100644 browser/components/newtab/content-src/asrouter/schemas/message-format.md create mode 100644 browser/components/newtab/content-src/asrouter/schemas/message-group.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/template-utils.js create mode 100644 browser/components/newtab/content-src/asrouter/templates/CFR/templates/CFRUrlbarChiclet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/CFR/templates/InfoBar.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/EOYSnippet/_EOYSnippet.scss create mode 100644 browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js create mode 100644 browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ProtectionsPanelMessage.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js create mode 100644 browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss create mode 100644 browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss create mode 100644 browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormScene2Snippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx create mode 100644 browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/_SubmitFormSnippet.scss create mode 100644 browser/components/newtab/content-src/asrouter/templates/ToastNotification/ToastNotification.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx create mode 100644 browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx create mode 100644 browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss create mode 100644 browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx create mode 100644 browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss create mode 100644 browser/components/newtab/content-src/components/ASRouterAdmin/CopyButton.jsx create mode 100644 browser/components/newtab/content-src/components/ASRouterAdmin/SimpleHashRouter.jsx create mode 100644 browser/components/newtab/content-src/components/Base/Base.jsx create mode 100644 browser/components/newtab/content-src/components/Base/_Base.scss create mode 100644 browser/components/newtab/content-src/components/Card/Card.jsx create mode 100644 browser/components/newtab/content-src/components/Card/_Card.scss create mode 100644 browser/components/newtab/content-src/components/Card/types.js create mode 100644 browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx create mode 100644 browser/components/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss create mode 100644 browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx create mode 100644 browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx create mode 100644 browser/components/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss create mode 100644 browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx create mode 100644 browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx create mode 100644 browser/components/newtab/content-src/components/ContextMenu/_ContextMenu.scss create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamBase/_DiscoveryStreamBase.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/_CollectionCardGrid.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSDismiss/_DSDismiss.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/_DSLinkMenu.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSMessage/_DSMessage.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/_DSPrivacyModal.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/HorizontalRule/_HorizontalRule.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/_Navigation.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/PrivacyLink/_PrivacyLink.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/SectionTitle/_SectionTitle.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopSites/_TopSites.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/_ImpressionStats.scss create mode 100644 browser/components/newtab/content-src/components/ErrorBoundary/ErrorBoundary.jsx create mode 100644 browser/components/newtab/content-src/components/ErrorBoundary/_ErrorBoundary.scss create mode 100644 browser/components/newtab/content-src/components/FluentOrText/FluentOrText.jsx create mode 100644 browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx create mode 100644 browser/components/newtab/content-src/components/MoreRecommendations/MoreRecommendations.jsx create mode 100644 browser/components/newtab/content-src/components/MoreRecommendations/_MoreRecommendations.scss create mode 100644 browser/components/newtab/content-src/components/PocketLoggedInCta/PocketLoggedInCta.jsx create mode 100644 browser/components/newtab/content-src/components/PocketLoggedInCta/_PocketLoggedInCta.scss create mode 100644 browser/components/newtab/content-src/components/Search/Search.jsx create mode 100644 browser/components/newtab/content-src/components/Search/_Search.scss create mode 100644 browser/components/newtab/content-src/components/Sections/Sections.jsx create mode 100644 browser/components/newtab/content-src/components/Sections/_Sections.scss create mode 100644 browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSite.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSiteFormInput.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSites.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js create mode 100644 browser/components/newtab/content-src/components/TopSites/_TopSites.scss create mode 100644 browser/components/newtab/content-src/components/Topics/Topics.jsx create mode 100644 browser/components/newtab/content-src/components/Topics/_Topics.scss create mode 100644 browser/components/newtab/content-src/lib/aboutwelcome-utils.js create mode 100644 browser/components/newtab/content-src/lib/constants.js create mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.js create mode 100644 browser/components/newtab/content-src/lib/init-store.js create mode 100644 browser/components/newtab/content-src/lib/link-menu-options.js create mode 100644 browser/components/newtab/content-src/lib/perf-service.js create mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.js create mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.js create mode 100644 browser/components/newtab/content-src/styles/_activity-stream.scss create mode 100644 browser/components/newtab/content-src/styles/_feature-callout-theme.scss create mode 100644 browser/components/newtab/content-src/styles/_feature-callout.scss create mode 100644 browser/components/newtab/content-src/styles/_icons.scss create mode 100644 browser/components/newtab/content-src/styles/_mixins.scss create mode 100644 browser/components/newtab/content-src/styles/_normalize.scss create mode 100644 browser/components/newtab/content-src/styles/_theme.scss create mode 100644 browser/components/newtab/content-src/styles/_variables.scss create mode 100644 browser/components/newtab/content-src/styles/activity-stream-linux.scss create mode 100644 browser/components/newtab/content-src/styles/activity-stream-mac.scss create mode 100644 browser/components/newtab/content-src/styles/activity-stream-windows.scss create mode 100644 browser/components/newtab/css/activity-stream-linux.css create mode 100644 browser/components/newtab/css/activity-stream-mac.css create mode 100644 browser/components/newtab/css/activity-stream-windows.css create mode 100644 browser/components/newtab/data/content/abouthomecache/page.html.template create mode 100644 browser/components/newtab/data/content/abouthomecache/script.js.template create mode 100644 browser/components/newtab/data/content/activity-stream.bundle.js create mode 100644 browser/components/newtab/data/content/assets/confetti.svg create mode 100644 browser/components/newtab/data/content/assets/default.svg create mode 100644 browser/components/newtab/data/content/assets/device-migration.svg create mode 100644 browser/components/newtab/data/content/assets/firefox.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-mail-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-maximize-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-minimize-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-newWindow-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-open-file-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-pin-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-unpin-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-webextension-16.svg create mode 100644 browser/components/newtab/data/content/assets/heart.webp create mode 100644 browser/components/newtab/data/content/assets/icon-removed-bookmark.svg create mode 100644 browser/components/newtab/data/content/assets/long-zap.svg create mode 100644 browser/components/newtab/data/content/assets/mobile-download-qr-existing-user-cn.svg create mode 100644 browser/components/newtab/data/content/assets/mobile-download-qr-existing-user.svg create mode 100644 browser/components/newtab/data/content/assets/mobile-download-qr-new-user-cn.svg create mode 100644 browser/components/newtab/data/content/assets/mobile-download-qr-new-user.svg create mode 100644 browser/components/newtab/data/content/assets/mr-colorways.avif create mode 100644 browser/components/newtab/data/content/assets/mr-gratitude.svg create mode 100644 browser/components/newtab/data/content/assets/mr-import.svg create mode 100644 browser/components/newtab/data/content/assets/mr-mobilecrosspromo.svg create mode 100644 browser/components/newtab/data/content/assets/mr-pinprivate.svg create mode 100644 browser/components/newtab/data/content/assets/mr-pintaskbar.svg create mode 100644 browser/components/newtab/data/content/assets/mr-privacysegmentation.svg create mode 100644 browser/components/newtab/data/content/assets/mr-rtamo-background-image.svg create mode 100644 browser/components/newtab/data/content/assets/mr-settodefault.svg create mode 100644 browser/components/newtab/data/content/assets/noodle-C.svg create mode 100644 browser/components/newtab/data/content/assets/noodle-outline-L.svg create mode 100644 browser/components/newtab/data/content/assets/noodle-solid-L.svg create mode 100644 browser/components/newtab/data/content/assets/person-typing.svg create mode 100644 browser/components/newtab/data/content/assets/pocket-onboarding.avif create mode 100644 browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif create mode 100644 browser/components/newtab/data/content/assets/pocket-swoosh.svg create mode 100644 browser/components/newtab/data/content/assets/remote/mountain.svg create mode 100644 browser/components/newtab/data/content/assets/remote/umbrella.png create mode 100644 browser/components/newtab/data/content/assets/short-zap.svg create mode 100644 browser/components/newtab/data/content/assets/spinner.svg create mode 100644 browser/components/newtab/data/content/newtab-render.js create mode 100644 browser/components/newtab/data/content/tippytop/favicons/adidas.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/amazon.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/baidu-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/bing-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ebay.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/etsy.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/geico.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/google-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/hrblock.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/nike.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/reddit-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/samsung.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/turbotax.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/vk-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/vodafone.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/wix.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/yandex-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/youtube-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/images/adidas@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/amazon@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg create mode 100644 browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg create mode 100644 browser/components/newtab/data/content/tippytop/images/ebay@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/geico@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/google-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/hrblock@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/nike@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/vk-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/wix@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/top_sites.json create mode 100644 browser/components/newtab/docs/index.rst create mode 100644 browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.md create mode 100644 browser/components/newtab/docs/v2-system-addon/data_dictionary.md create mode 100644 browser/components/newtab/docs/v2-system-addon/data_events.md create mode 100644 browser/components/newtab/docs/v2-system-addon/geo_locale.md create mode 100644 browser/components/newtab/docs/v2-system-addon/mochitests.md create mode 100644 browser/components/newtab/docs/v2-system-addon/preferences.md create mode 100644 browser/components/newtab/docs/v2-system-addon/remote_cfr.md create mode 100644 browser/components/newtab/docs/v2-system-addon/sections.md create mode 100644 browser/components/newtab/docs/v2-system-addon/telemetry.md create mode 100644 browser/components/newtab/docs/v2-system-addon/tippytop.md create mode 100644 browser/components/newtab/docs/v2-system-addon/unit_testing_guide.md create mode 100644 browser/components/newtab/jar.mn create mode 100644 browser/components/newtab/karma.mc.config.js create mode 100644 browser/components/newtab/lib/ASRouter.jsm create mode 100644 browser/components/newtab/lib/ASRouterDefaultConfig.jsm create mode 100644 browser/components/newtab/lib/ASRouterNewTabHook.sys.mjs create mode 100644 browser/components/newtab/lib/ASRouterParentProcessMessageHandler.jsm create mode 100644 browser/components/newtab/lib/ASRouterPreferences.jsm create mode 100644 browser/components/newtab/lib/ASRouterTargeting.jsm create mode 100644 browser/components/newtab/lib/ASRouterTriggerListeners.jsm create mode 100644 browser/components/newtab/lib/AWScreenUtils.jsm create mode 100644 browser/components/newtab/lib/AboutPreferences.jsm create mode 100644 browser/components/newtab/lib/ActivityStream.jsm create mode 100644 browser/components/newtab/lib/ActivityStreamMessageChannel.jsm create mode 100644 browser/components/newtab/lib/ActivityStreamPrefs.jsm create mode 100644 browser/components/newtab/lib/ActivityStreamStorage.jsm create mode 100644 browser/components/newtab/lib/CFRMessageProvider.sys.mjs create mode 100644 browser/components/newtab/lib/CFRPageActions.jsm create mode 100644 browser/components/newtab/lib/DefaultSites.sys.mjs create mode 100644 browser/components/newtab/lib/DiscoveryStreamFeed.jsm create mode 100644 browser/components/newtab/lib/DownloadsManager.jsm create mode 100644 browser/components/newtab/lib/FaviconFeed.jsm create mode 100644 browser/components/newtab/lib/FeatureCalloutMessages.sys.mjs create mode 100644 browser/components/newtab/lib/FilterAdult.jsm create mode 100644 browser/components/newtab/lib/HighlightsFeed.jsm create mode 100644 browser/components/newtab/lib/InfoBar.jsm create mode 100644 browser/components/newtab/lib/LinksCache.sys.mjs create mode 100644 browser/components/newtab/lib/MomentsPageHub.jsm create mode 100644 browser/components/newtab/lib/NewTabInit.jsm create mode 100644 browser/components/newtab/lib/OnboardingMessageProvider.jsm create mode 100644 browser/components/newtab/lib/PageEventManager.sys.mjs create mode 100644 browser/components/newtab/lib/PanelTestProvider.sys.mjs create mode 100644 browser/components/newtab/lib/PersistentCache.sys.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/NaiveBayesTextTagger.jsm create mode 100644 browser/components/newtab/lib/PersonalityProvider/NmfTextTagger.jsm create mode 100644 browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.jsm create mode 100644 browser/components/newtab/lib/PersonalityProvider/PersonalityProviderWorker.js create mode 100644 browser/components/newtab/lib/PersonalityProvider/PersonalityProviderWorkerClass.jsm create mode 100644 browser/components/newtab/lib/PersonalityProvider/RecipeExecutor.jsm create mode 100644 browser/components/newtab/lib/PersonalityProvider/Tokenize.jsm create mode 100644 browser/components/newtab/lib/PlacesFeed.jsm create mode 100644 browser/components/newtab/lib/PrefsFeed.jsm create mode 100644 browser/components/newtab/lib/RecommendationProvider.jsm create mode 100644 browser/components/newtab/lib/RemoteL10n.sys.mjs create mode 100644 browser/components/newtab/lib/Screenshots.jsm create mode 100644 browser/components/newtab/lib/SearchShortcuts.sys.mjs create mode 100644 browser/components/newtab/lib/SectionsManager.jsm create mode 100644 browser/components/newtab/lib/ShortURL.jsm create mode 100644 browser/components/newtab/lib/SiteClassifier.jsm create mode 100644 browser/components/newtab/lib/SnippetsTestMessageProvider.sys.mjs create mode 100644 browser/components/newtab/lib/Spotlight.jsm create mode 100644 browser/components/newtab/lib/Store.jsm create mode 100644 browser/components/newtab/lib/SystemTickFeed.jsm create mode 100644 browser/components/newtab/lib/TelemetryFeed.jsm create mode 100644 browser/components/newtab/lib/TippyTopProvider.sys.mjs create mode 100644 browser/components/newtab/lib/ToastNotification.jsm create mode 100644 browser/components/newtab/lib/ToolbarBadgeHub.jsm create mode 100644 browser/components/newtab/lib/ToolbarPanelHub.jsm create mode 100644 browser/components/newtab/lib/TopSitesFeed.jsm create mode 100644 browser/components/newtab/lib/TopStoriesFeed.jsm create mode 100644 browser/components/newtab/lib/UTEventReporting.sys.mjs create mode 100644 browser/components/newtab/lib/cache-worker.js create mode 100644 browser/components/newtab/loaders/inject-loader.js create mode 100644 browser/components/newtab/metrics.yaml create mode 100644 browser/components/newtab/moz.build create mode 100644 browser/components/newtab/nsIAboutNewTabService.idl create mode 100644 browser/components/newtab/package-lock.json create mode 100644 browser/components/newtab/package.json create mode 100644 browser/components/newtab/pings.yaml create mode 100644 browser/components/newtab/prerendered/activity-stream-debug.html create mode 100644 browser/components/newtab/prerendered/activity-stream-noscripts.html create mode 100644 browser/components/newtab/prerendered/activity-stream.html create mode 100644 browser/components/newtab/test/.eslintrc.js create mode 100644 browser/components/newtab/test/InflightAssetsMessageProvider.jsm create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser.ini create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_disabled.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/head.js create mode 100644 browser/components/newtab/test/browser/annotation_first.html create mode 100644 browser/components/newtab/test/browser/annotation_second.html create mode 100644 browser/components/newtab/test/browser/annotation_third.html create mode 100644 browser/components/newtab/test/browser/blue_page.html create mode 100644 browser/components/newtab/test/browser/browser.ini create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_attribution.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_configurable_ui.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_fxa_signin_flow.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_glean.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_import.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_mobile_downloads.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_multistage_default.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_multistage_experimentAPI.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_multistage_languageSwitcher.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_multistage_mr.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_multistage_video.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_observer.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_rtamo.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_screen_targeting.js create mode 100644 browser/components/newtab/test/browser/browser_aboutwelcome_upgrade_multistage_mr.js create mode 100644 browser/components/newtab/test/browser/browser_as_load_location.js create mode 100644 browser/components/newtab/test/browser/browser_as_render.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_bug1761522.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_bug1800087.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_cfr.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_group_frequency.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_group_userprefs.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_infobar.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_momentspagehub.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_snippets.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_snippets_dismiss.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_targeting.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_toast_notification.js create mode 100644 browser/components/newtab/test/browser/browser_asrouter_toolbarbadge.js create mode 100644 browser/components/newtab/test/browser/browser_context_menu_item.js create mode 100644 browser/components/newtab/test/browser/browser_customize_menu_content.js create mode 100644 browser/components/newtab/test/browser/browser_customize_menu_render.js create mode 100644 browser/components/newtab/test/browser/browser_discovery_card.js create mode 100644 browser/components/newtab/test/browser/browser_discovery_render.js create mode 100644 browser/components/newtab/test/browser/browser_discovery_styles.js create mode 100644 browser/components/newtab/test/browser/browser_enabled_newtabpage.js create mode 100644 browser/components/newtab/test/browser/browser_feature_callout_in_chrome.js create mode 100644 browser/components/newtab/test/browser/browser_getScreenshots.js create mode 100644 browser/components/newtab/test/browser/browser_highlights_section.js create mode 100644 browser/components/newtab/test/browser/browser_multistage_spotlight.js create mode 100644 browser/components/newtab/test/browser/browser_multistage_spotlight_telemetry.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_header.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_last_LinkMenu.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_overrides.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_ping.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_towindow.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_trigger.js create mode 100644 browser/components/newtab/test/browser/browser_open_tab_focus.js create mode 100644 browser/components/newtab/test/browser/browser_remote_l10n.js create mode 100644 browser/components/newtab/test/browser/browser_topsites_annotation.js create mode 100644 browser/components/newtab/test/browser/browser_topsites_contextMenu_options.js create mode 100644 browser/components/newtab/test/browser/browser_topsites_section.js create mode 100644 browser/components/newtab/test/browser/browser_trigger_listeners.js create mode 100644 browser/components/newtab/test/browser/browser_trigger_messagesLoaded.js create mode 100644 browser/components/newtab/test/browser/ds_layout.json create mode 100644 browser/components/newtab/test/browser/file_pdf.PDF create mode 100644 browser/components/newtab/test/browser/head.js create mode 100644 browser/components/newtab/test/browser/red_page.html create mode 100644 browser/components/newtab/test/browser/redirect_to.sjs create mode 100644 browser/components/newtab/test/browser/snippet.json create mode 100644 browser/components/newtab/test/browser/snippet_below_search_test.json create mode 100644 browser/components/newtab/test/browser/snippet_simple_test.json create mode 100644 browser/components/newtab/test/browser/topstories.json create mode 100644 browser/components/newtab/test/schemas/pings.js create mode 100644 browser/components/newtab/test/unit/aboutwelcome/AWScreenUtils.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/CTAParagraph.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/HeroImage.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/MRColorways.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/MobileDownloads.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/MultiSelect.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/MultiStageAWProton.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/MultiStageAboutWelcome.test.jsx create mode 100644 browser/components/newtab/test/unit/aboutwelcome/OnboardingVideoTest.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouter.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterChild.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterNewTabHook.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterParent.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterParentProcessMessageHandler.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterTargeting.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ASRouterTriggerListeners.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/ModalOverlay.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/RemoteL10n.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/RichText.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/SnippetsTestMessageProvider.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/TargetingDocs.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/asrouter-utils.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/compatibility-reference/fx57-compat.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/compatibility-reference/snippets-fx57.js create mode 100644 browser/components/newtab/test/unit/asrouter/constants.js create mode 100644 browser/components/newtab/test/unit/asrouter/template-utils.test.js create mode 100644 browser/components/newtab/test/unit/asrouter/templates/EOYSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/FXASignupSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/NewsletterSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/SendToDeviceSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx create mode 100644 browser/components/newtab/test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js create mode 100644 browser/components/newtab/test/unit/common/Actions.test.js create mode 100644 browser/components/newtab/test/unit/common/Dedupe.test.js create mode 100644 browser/components/newtab/test/unit/common/Reducers.test.js create mode 100644 browser/components/newtab/test/unit/content-src/components/ASRouterAdmin.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Base.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Card.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/CollapsibleSection.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ContextMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamBase.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CollectionCardGrid.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSSignup.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/PrivacyLink.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ErrorBoundary.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/FluentOrText.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/HelpText.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/LinkMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/MSLocalized.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/MoreRecommendations.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/PocketLoggedInCta.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Search.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Sections.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/TopSites/SearchShortcutsForm.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Topics.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/addUtmParams.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/init-store.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/perf-service.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/screenshot-utils.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js create mode 100644 browser/components/newtab/test/unit/lib/AboutPreferences.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStream.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStreamPrefs.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js create mode 100644 browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/DownloadsManager.test.js create mode 100644 browser/components/newtab/test/unit/lib/FaviconFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/FilterAdult.test.js create mode 100644 browser/components/newtab/test/unit/lib/HighlightsFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/LinksCache.test.js create mode 100644 browser/components/newtab/test/unit/lib/MomentsPageHub.test.js create mode 100644 browser/components/newtab/test/unit/lib/NewTabInit.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersistentCache.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/NaiveBayesTextTagger.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/NmfTextTagger.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProvider.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/RecipeExecutor.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/Tokenize.test.js create mode 100644 browser/components/newtab/test/unit/lib/PlacesFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/PrefsFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/RecommendationProvider.test.js create mode 100644 browser/components/newtab/test/unit/lib/Screenshots.test.js create mode 100644 browser/components/newtab/test/unit/lib/SectionsManager.test.js create mode 100644 browser/components/newtab/test/unit/lib/ShortUrl.test.js create mode 100644 browser/components/newtab/test/unit/lib/SiteClassifier.test.js create mode 100644 browser/components/newtab/test/unit/lib/Store.test.js create mode 100644 browser/components/newtab/test/unit/lib/SystemTickFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/TelemetryFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/TippyTopProvider.test.js create mode 100644 browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js create mode 100644 browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js create mode 100644 browser/components/newtab/test/unit/lib/TopSitesFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/TopStoriesFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/UTEventReporting.test.js create mode 100644 browser/components/newtab/test/unit/unit-entry.js create mode 100644 browser/components/newtab/test/unit/utils.js create mode 100644 browser/components/newtab/test/xpcshell/ds_layout.json create mode 100644 browser/components/newtab/test/xpcshell/head.js create mode 100644 browser/components/newtab/test/xpcshell/test_ASRouterTargeting_attribution.js create mode 100644 browser/components/newtab/test/xpcshell/test_ASRouterTargeting_snapshot.js create mode 100644 browser/components/newtab/test/xpcshell/test_ASRouter_getTargetingParameters.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutNewTab.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js create mode 100644 browser/components/newtab/test/xpcshell/test_CFRMessageProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_InflightAssetsMessageProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_OnboardingMessageProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_PanelTestProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_reach_experiments.js create mode 100644 browser/components/newtab/test/xpcshell/test_remoteExperiments.js create mode 100644 browser/components/newtab/test/xpcshell/topstories.json create mode 100644 browser/components/newtab/test/xpcshell/xpcshell.ini create mode 100644 browser/components/newtab/tools/babel-jsm-to-commonjs.js create mode 100644 browser/components/newtab/tools/resourceUriPlugin.js create mode 100644 browser/components/newtab/vendor/PROP_TYPES_LICENSE create mode 100644 browser/components/newtab/vendor/REACT_AND_REACT_DOM_LICENSE create mode 100644 browser/components/newtab/vendor/REACT_REDUX_LICENSE create mode 100644 browser/components/newtab/vendor/REACT_TRANSITION_GROUP_LICENSE create mode 100644 browser/components/newtab/vendor/REDUX_LICENSE create mode 100644 browser/components/newtab/vendor/Redux.jsm create mode 100644 browser/components/newtab/vendor/prop-types.js create mode 100644 browser/components/newtab/vendor/react-dev.js create mode 100644 browser/components/newtab/vendor/react-dom-dev.js create mode 100644 browser/components/newtab/vendor/react-dom-server.js create mode 100644 browser/components/newtab/vendor/react-dom.js create mode 100644 browser/components/newtab/vendor/react-redux.js create mode 100644 browser/components/newtab/vendor/react-transition-group.js create mode 100644 browser/components/newtab/vendor/react.js create mode 100644 browser/components/newtab/vendor/redux.js create mode 100644 browser/components/newtab/webpack.aboutwelcome.config.js create mode 100644 browser/components/newtab/webpack.system-addon.config.js create mode 100644 browser/components/newtab/yamscripts.yml (limited to 'browser/components/newtab') diff --git a/browser/components/newtab/.eslintrc.js b/browser/components/newtab/.eslintrc.js new file mode 100644 index 0000000000..b6362a9552 --- /dev/null +++ b/browser/components/newtab/.eslintrc.js @@ -0,0 +1,211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +module.exports = { + // When adding items to this file please check for effects on sub-directories. + parserOptions: { + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + sourceType: "module", + }, + plugins: [ + "import", // require("eslint-plugin-import") + "react", // require("eslint-plugin-react") + "jsx-a11y", // require("eslint-plugin-jsx-a11y") + ], + settings: { + react: { + version: "16.2.0", + }, + }, + extends: [ + "eslint:recommended", + "plugin:jsx-a11y/recommended", // require("eslint-plugin-jsx-a11y") + "plugin:mozilla/recommended", // require("eslint-plugin-mozilla") require("eslint-plugin-fetch-options") require("eslint-plugin-html") require("eslint-plugin-no-unsanitized") + "prettier", // require("eslint-config-prettier") + ], + overrides: [ + { + // These files use fluent-dom to insert content + files: [ + "content-src/aboutwelcome/components/Zap.jsx", + "content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx", + "content-src/aboutwelcome/components/MultiStageScreen.jsx", + "content-src/aboutwelcome/components/MultiStageProtonScreen.jsx", + "content-src/aboutwelcome/components/MultiSelect.jsx", + "content-src/aboutwelcome/components/ReturnToAMO.jsx", + "content-src/asrouter/templates/OnboardingMessage/**", + "content-src/asrouter/templates/FirstRun/**", + "content-src/components/TopSites/**", + "content-src/components/MoreRecommendations/MoreRecommendations.jsx", + "content-src/components/CollapsibleSection/CollapsibleSection.jsx", + "content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx", + "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx", + "content-src/components/CustomizeMenu/**", + ], + rules: { + "jsx-a11y/anchor-has-content": "off", + "jsx-a11y/heading-has-content": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/no-onchange": "off", + }, + }, + { + files: [ + "bin/**", + "content-src/**", + "./*.js", + "loaders/**", + "tools/**", + "test/unit/**", + ], + env: { + node: true, + }, + }, + { + // Use a configuration that's more appropriate for JSMs + files: "**/*.jsm", + parserOptions: { + sourceType: "script", + }, + rules: { + "no-implicit-globals": "off", + }, + }, + { + files: "test/xpcshell/**", + extends: ["plugin:mozilla/xpcshell-test"], + }, + { + files: "test/browser/**", + extends: ["plugin:mozilla/browser-test"], + }, + { + // Exempt all files without a 'test' string in their path name since no-insecure-url + // is focussing on the test base + files: "*", + excludedFiles: ["**/test**", "**/test*/**", "Test*/**"], + rules: { + "@microsoft/sdl/no-insecure-url": "off", + }, + }, + { + // That are all files in browser/component/newtab/test that produces warnings in the existing test infrastructure. + // Since our focus is that new tests won't use http without thinking twice we exempt + // these test files for now. + // TODO gradually check and remove from here bug 1758951. + files: [ + "browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js", + "browser/components/newtab/test/browser/browser_aboutwelcome_observer.js", + "browser/components/newtab/test/browser/browser_asrouter_cfr.js", + "browser/components/newtab/test/browser/browser_asrouter_group_frequency.js", + "browser/components/newtab/test/browser/browser_asrouter_group_userprefs.js", + "browser/components/newtab/test/browser/browser_trigger_listeners.js", + ], + rules: { + "@microsoft/sdl/no-insecure-url": "off", + }, + }, + ], + rules: { + "fetch-options/no-fetch-credentials": "error", + + "react/jsx-boolean-value": ["error", "always"], + "react/jsx-key": "error", + "react/jsx-no-bind": "error", + "react/jsx-no-comment-textnodes": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-target-blank": "error", + "react/jsx-no-undef": "error", + "react/jsx-pascal-case": "error", + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "react/no-access-state-in-setstate": "error", + "react/no-danger": "error", + "react/no-deprecated": "error", + "react/no-did-mount-set-state": "error", + "react/no-did-update-set-state": "error", + "react/no-direct-mutation-state": "error", + "react/no-is-mounted": "error", + "react/no-unknown-property": "error", + "react/require-render-return": "error", + + "accessor-pairs": ["error", { setWithoutGet: true, getWithoutSet: false }], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-this": ["error", "use-bind"], + eqeqeq: "error", + "for-direction": "error", + "func-name-matching": "error", + "getter-return": "error", + "guard-for-in": "error", + "handle-callback-err": "error", + "lines-between-class-members": "error", + "max-depth": ["error", 4], + "max-nested-callbacks": ["error", 4], + "max-params": ["error", 6], + "max-statements": ["error", 50], + "max-statements-per-line": ["error", { max: 2 }], + "new-cap": ["error", { newIsCap: true, capIsNew: false }], + "no-alert": "error", + "no-buffer-constructor": "error", + "no-console": ["error", { allow: ["error"] }], + "no-div-regex": "error", + "no-duplicate-imports": "error", + "no-eq-null": "error", + "no-extend-native": "error", + "no-extra-label": "error", + "no-implicit-coercion": ["error", { allow: ["!!"] }], + "no-implicit-globals": "error", + "no-loop-func": "error", + "no-mixed-requires": "error", + "no-multi-assign": "error", + "no-multi-str": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-require": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-return-assign": ["error", "except-parens"], + "no-script-url": "error", + "no-shadow": "error", + "no-template-curly-in-string": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-unused-expressions": "error", + "no-use-before-define": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-var": "error", + "no-void": ["error", { allowAsStatement: true }], + "one-var": ["error", "never"], + "operator-assignment": ["error", "always"], + "prefer-destructuring": [ + "error", + { + AssignmentExpression: { array: true }, + VariableDeclarator: { array: true, object: true }, + }, + ], + "prefer-numeric-literals": "error", + "prefer-promise-reject-errors": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + radix: ["error", "always"], + "require-await": "error", + "sort-vars": "error", + "symbol-description": "error", + "vars-on-top": "error", + yoda: ["error", "never"], + }, +}; diff --git a/browser/components/newtab/.nvmrc b/browser/components/newtab/.nvmrc new file mode 100644 index 0000000000..e65243f2ea --- /dev/null +++ b/browser/components/newtab/.nvmrc @@ -0,0 +1 @@ +16.19.0 diff --git a/browser/components/newtab/AboutNewTabService.jsm b/browser/components/newtab/AboutNewTabService.jsm new file mode 100644 index 0000000000..5b9eb478df --- /dev/null +++ b/browser/components/newtab/AboutNewTabService.jsm @@ -0,0 +1,523 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const EXPORTED_SYMBOLS = [ + "AboutNewTabStubService", + "AboutHomeStartupCacheChild", +]; + +/** + * The nsIAboutNewTabService is accessed by the AboutRedirector anytime + * about:home, about:newtab or about:welcome are requested. The primary + * job of an nsIAboutNewTabService is to tell the AboutRedirector what + * resources to actually load for those requests. + * + * The nsIAboutNewTabService is not involved when the user has overridden + * the default about:home or about:newtab pages. + * + * There are two implementations of this service - one for the parent + * process, and one for content processes. Each one has some secondary + * responsibilties that are process-specific. + * + * The need for two implementations is an unfortunate consequence of how + * document loading and process redirection for about: pages currently + * works in Gecko. The commonalities between the two implementations has + * been put into an abstract base class. + */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +/** + * BEWARE: Do not add variables for holding state in the global scope. + * Any state variables should be properties of the appropriate class + * below. This is to avoid confusion where the state is set in one process, + * but not in another. + * + * Constants are fine in the global scope. + */ + +const PREF_ABOUT_HOME_CACHE_TESTING = + "browser.startup.homepage.abouthome_cache.testing"; +const ABOUT_WELCOME_URL = + "resource://activity-stream/aboutwelcome/aboutwelcome.html"; + +const CACHE_WORKER_URL = "resource://activity-stream/lib/cache-worker.js"; + +const IS_PRIVILEGED_PROCESS = + Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE; + +const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS = + "browser.tabs.remote.separatePrivilegedContentProcess"; +const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug"; + +/** + * The AboutHomeStartupCacheChild is responsible for connecting the + * nsIAboutNewTabService with a cached document and script for about:home + * if one happens to exist. The AboutHomeStartupCacheChild is only ever + * handed the streams for those caches when the "privileged about content + * process" first launches, so subsequent loads of about:home do not read + * from this cache. + * + * See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html + * for further details. + */ +const AboutHomeStartupCacheChild = { + _initted: false, + CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest", + CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse", + CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult", + STATES: { + UNAVAILABLE: 0, + UNCONSUMED: 1, + PAGE_CONSUMED: 2, + PAGE_AND_SCRIPT_CONSUMED: 3, + FAILED: 4, + DISQUALIFIED: 5, + }, + REQUEST_TYPE: { + PAGE: 0, + SCRIPT: 1, + }, + _state: 0, + _consumerBCID: null, + + /** + * Called via a process script very early on in the process lifetime. This + * prepares the AboutHomeStartupCacheChild to pass an nsIChannel back to + * the nsIAboutNewTabService when the initial about:home document is + * eventually requested. + * + * @param pageInputStream (nsIInputStream) + * The stream for the cached page markup. + * @param scriptInputStream (nsIInputStream) + * The stream for the cached script to run on the page. + */ + init(pageInputStream, scriptInputStream) { + if ( + !IS_PRIVILEGED_PROCESS && + !Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false) + ) { + throw new Error( + "Can only instantiate in the privileged about content processes." + ); + } + + if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) { + return; + } + + if (this._initted) { + throw new Error("AboutHomeStartupCacheChild already initted."); + } + + Services.obs.addObserver(this, "memory-pressure"); + Services.cpmm.addMessageListener(this.CACHE_REQUEST_MESSAGE, this); + + this._pageInputStream = pageInputStream; + this._scriptInputStream = scriptInputStream; + this._initted = true; + this.setState(this.STATES.UNCONSUMED); + }, + + /** + * A function that lets us put the AboutHomeStartupCacheChild back into + * its initial state. This is used by tests to let us simulate the startup + * behaviour of the module without having to manually launch a new privileged + * about content process every time. + */ + uninit() { + if (!Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)) { + throw new Error( + "Cannot uninit AboutHomeStartupCacheChild unless testing." + ); + } + + if (!this._initted) { + return; + } + + Services.obs.removeObserver(this, "memory-pressure"); + Services.cpmm.removeMessageListener(this.CACHE_REQUEST_MESSAGE, this); + + if (this._cacheWorker) { + this._cacheWorker.terminate(); + this._cacheWorker = null; + } + + this._pageInputStream = null; + this._scriptInputStream = null; + this._initted = false; + this._state = this.STATES.UNAVAILABLE; + this._consumerBCID = null; + }, + + /** + * A public method called from nsIAboutNewTabService that attempts + * return an nsIChannel for a cached about:home document that we + * were initialized with. If we failed to be initted with the + * cache, or the input streams that we were sent have no data + * yet available, this function returns null. The caller should + * fall back to generating the page dynamically. + * + * This function will be called when loading about:home, or + * about:home?jscache - the latter returns the cached script. + * + * It is expected that the same BrowsingContext that loads the cached + * page will also load the cached script. + * + * @param uri (nsIURI) + * The URI for the requested page, as passed by nsIAboutNewTabService. + * @param loadInfo (nsILoadInfo) + * The nsILoadInfo for the requested load, as passed by + * nsIAboutNewWTabService. + * @return nsIChannel or null. + */ + maybeGetCachedPageChannel(uri, loadInfo) { + if (!this._initted) { + return null; + } + + if (this._state >= this.STATES.PAGE_AND_SCRIPT_CONSUMED) { + return null; + } + + let requestType = + uri.query === "jscache" + ? this.REQUEST_TYPE.SCRIPT + : this.REQUEST_TYPE.PAGE; + + // If this is a page request, then we need to be in the UNCONSUMED state, + // since we expect the page request to come first. If this is a script + // request, we expect to be in PAGE_CONSUMED state, since the page cache + // stream should he been consumed already. + if ( + (requestType === this.REQUEST_TYPE.PAGE && + this._state !== this.STATES.UNCONSUMED) || + (requestType === this.REQUEST_TYPE_SCRIPT && + this._state !== this.STATES.PAGE_CONSUMED) + ) { + return null; + } + + // If by this point, we don't have anything in the streams, + // then either the cache was too slow to give us data, or the cache + // doesn't exist. The caller should fall back to generating the + // page dynamically. + // + // We only do this on the page request, because by the time + // we get to the script request, we should have already drained + // the page input stream. + if (requestType === this.REQUEST_TYPE.PAGE) { + try { + if ( + !this._scriptInputStream.available() || + !this._pageInputStream.available() + ) { + this.setState(this.STATES.FAILED); + this.reportUsageResult(false /* success */); + return null; + } + } catch (e) { + this.setState(this.STATES.FAILED); + if (e.result === Cr.NS_BASE_STREAM_CLOSED) { + this.reportUsageResult(false /* success */); + return null; + } + throw e; + } + } + + if ( + requestType === this.REQUEST_TYPE.SCRIPT && + this._consumerBCID !== loadInfo.browsingContextID + ) { + // Some other document is somehow requesting the script - one + // that didn't originally request the page. This is not allowed. + this.setState(this.STATES.FAILED); + return null; + } + + let channel = Cc[ + "@mozilla.org/network/input-stream-channel;1" + ].createInstance(Ci.nsIInputStreamChannel); + channel.QueryInterface(Ci.nsIChannel); + channel.setURI(uri); + channel.loadInfo = loadInfo; + channel.contentStream = + requestType === this.REQUEST_TYPE.PAGE + ? this._pageInputStream + : this._scriptInputStream; + + if (requestType === this.REQUEST_TYPE.SCRIPT) { + this.setState(this.STATES.PAGE_AND_SCRIPT_CONSUMED); + this.reportUsageResult(true /* success */); + } else { + this.setState(this.STATES.PAGE_CONSUMED); + // Stash the BrowsingContext ID so that when the script stream + // attempts to be consumed, we ensure that it's from the same + // BrowsingContext that loaded the page. + this._consumerBCID = loadInfo.browsingContextID; + } + + return channel; + }, + + /** + * This function takes the state information required to generate + * the about:home cache markup and script, and then generates that + * markup in script asynchronously. Once that's done, a message + * is sent to the parent process with the nsIInputStream's for the + * markup and script contents. + * + * @param state (Object) + * The Redux state of the about:home document to render. + * @return Promise + * @resolves undefined + * After the message with the nsIInputStream's have been sent to + * the parent. + */ + async constructAndSendCache(state) { + if (!IS_PRIVILEGED_PROCESS) { + throw new Error("Wrong process type."); + } + + let worker = this.getOrCreateWorker(); + + TelemetryStopwatch.start("FX_ABOUTHOME_CACHE_CONSTRUCTION"); + + let { page, script } = await worker + .post("construct", [state]) + .finally(() => { + TelemetryStopwatch.finish("FX_ABOUTHOME_CACHE_CONSTRUCTION"); + }); + + let pageInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + pageInputStream.setUTF8Data(page); + + let scriptInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + scriptInputStream.setUTF8Data(script); + + Services.cpmm.sendAsyncMessage(this.CACHE_RESPONSE_MESSAGE, { + pageInputStream, + scriptInputStream, + }); + }, + + _cacheWorker: null, + getOrCreateWorker() { + if (this._cacheWorker) { + return this._cacheWorker; + } + + this._cacheWorker = new lazy.BasePromiseWorker(CACHE_WORKER_URL); + return this._cacheWorker; + }, + + receiveMessage(message) { + if (message.name === this.CACHE_REQUEST_MESSAGE) { + let { state } = message.data; + this.constructAndSendCache(state); + } + }, + + reportUsageResult(success) { + Services.cpmm.sendAsyncMessage(this.CACHE_USAGE_RESULT_MESSAGE, { + success, + }); + }, + + observe(subject, topic, data) { + if (topic === "memory-pressure" && this._cacheWorker) { + this._cacheWorker.terminate(); + this._cacheWorker = null; + } + }, + + /** + * Transitions the AboutHomeStartupCacheChild from one state + * to the next, where each state is defined in this.STATES. + * + * States can only be transitioned in increasing order, otherwise + * an error is logged. + */ + setState(state) { + if (state > this._state) { + this._state = state; + } else { + console.error( + "AboutHomeStartupCacheChild could not transition from state " + + `${this._state} to ${state}`, + new Error().stack + ); + } + }, + + /** + * If the cache hasn't been used, transitions it into the DISQUALIFIED + * state so that it cannot be used. This should be called if it's been + * determined that about:newtab is going to be loaded, which doesn't + * use the cache. + */ + disqualifyCache() { + if (this._state === this.STATES.UNCONSUMED) { + this.setState(this.STATES.DISQUALIFIED); + this.reportUsageResult(false /* success */); + } + }, +}; + +/** + * This is an abstract base class for the nsIAboutNewTabService + * implementations that has some common methods and properties. + */ +class BaseAboutNewTabService { + constructor() { + if (!AppConstants.RELEASE_OR_BETA) { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "activityStreamDebug", + PREF_ACTIVITY_STREAM_DEBUG, + false + ); + } else { + this.activityStreamDebug = false; + } + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "privilegedAboutProcessEnabled", + PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, + false + ); + + this.classID = Components.ID("{cb36c925-3adc-49b3-b720-a5cc49d8a40e}"); + this.QueryInterface = ChromeUtils.generateQI([ + "nsIAboutNewTabService", + "nsIObserver", + ]); + } + + /** + * Returns the default URL. + * + * This URL depends on various activity stream prefs. Overriding + * the newtab page has no effect on the result of this function. + */ + get defaultURL() { + // Generate the desired activity stream resource depending on state, e.g., + // "resource://activity-stream/prerendered/activity-stream.html" + // "resource://activity-stream/prerendered/activity-stream-debug.html" + // "resource://activity-stream/prerendered/activity-stream-noscripts.html" + return [ + "resource://activity-stream/prerendered/", + "activity-stream", + // Debug version loads dev scripts but noscripts separately loads scripts + this.activityStreamDebug && !this.privilegedAboutProcessEnabled + ? "-debug" + : "", + this.privilegedAboutProcessEnabled ? "-noscripts" : "", + ".html", + ].join(""); + } + + get welcomeURL() { + /* + * Returns the about:welcome URL + * + * This is calculated in the same way the default URL is. + */ + + lazy.NimbusFeatures.aboutwelcome.recordExposureEvent({ once: true }); + if (lazy.NimbusFeatures.aboutwelcome.getVariable("enabled") ?? true) { + return ABOUT_WELCOME_URL; + } + return this.defaultURL; + } + + aboutHomeChannel(uri, loadInfo) { + throw Components.Exception( + "AboutHomeChannel not implemented for this process.", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } +} + +/** + * The child-process implementation of nsIAboutNewTabService, + * which also does the work of redirecting about:home loads to + * the about:home startup cache if its available. + */ +class AboutNewTabChildService extends BaseAboutNewTabService { + aboutHomeChannel(uri, loadInfo) { + if (IS_PRIVILEGED_PROCESS) { + let cacheChannel = AboutHomeStartupCacheChild.maybeGetCachedPageChannel( + uri, + loadInfo + ); + if (cacheChannel) { + return cacheChannel; + } + } + + let pageURI = Services.io.newURI(this.defaultURL); + let fileChannel = Services.io.newChannelFromURIWithLoadInfo( + pageURI, + loadInfo + ); + fileChannel.originalURI = uri; + return fileChannel; + } + + get defaultURL() { + if (IS_PRIVILEGED_PROCESS) { + // This is a bit of a hack, but attempting to load about:newtab will + // enter this code path in order to get at the expected URL, and we + // can use that to disqualify the about:home cache, since we don't + // use it for about:newtab loads, and we don't want the about:home + // cache to be wildly out of date when about:home is eventually + // loaded (for example, in the first new window). + AboutHomeStartupCacheChild.disqualifyCache(); + } + + return super.defaultURL; + } +} + +/** + * The AboutNewTabStubService is a function called in both the main and + * content processes when trying to get at the nsIAboutNewTabService. This + * function does the job of choosing the appropriate implementation of + * nsIAboutNewTabService for the process type. + */ +function AboutNewTabStubService() { + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) { + return new BaseAboutNewTabService(); + } + return new AboutNewTabChildService(); +} diff --git a/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm b/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm new file mode 100644 index 0000000000..eef6c75b37 --- /dev/null +++ b/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm @@ -0,0 +1,339 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["AboutWelcomeChild"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AboutWelcomeDefaults: + "resource://activity-stream/aboutwelcome/lib/AboutWelcomeDefaults.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "log", () => { + const { Logger } = ChromeUtils.importESModule( + "resource://messaging-system/lib/Logger.sys.mjs" + ); + return new Logger("AboutWelcomeChild"); +}); + +async function getSelectedTheme(child) { + let activeThemeId = await child.sendQuery("AWPage:GET_SELECTED_THEME"); + return activeThemeId; +} + +class AboutWelcomeChild extends JSWindowActorChild { + actorCreated() { + this.exportFunctions(); + } + + /** + * Send event that can be handled by the page + * @param {{type: string, data?: any}} action + */ + sendToPage(action) { + lazy.log.debug(`Sending to page: ${action.type}`); + const win = this.document.defaultView; + const event = new win.CustomEvent("AboutWelcomeChromeToContent", { + detail: Cu.cloneInto(action, win), + }); + win.dispatchEvent(event); + } + + /** + * Export functions that can be called by page js + */ + exportFunctions() { + let window = this.contentWindow; + + Cu.exportFunction(this.AWAddScreenImpression.bind(this), window, { + defineAs: "AWAddScreenImpression", + }); + + Cu.exportFunction(this.AWGetFeatureConfig.bind(this), window, { + defineAs: "AWGetFeatureConfig", + }); + + Cu.exportFunction(this.AWGetFxAMetricsFlowURI.bind(this), window, { + defineAs: "AWGetFxAMetricsFlowURI", + }); + + Cu.exportFunction(this.AWGetSelectedTheme.bind(this), window, { + defineAs: "AWGetSelectedTheme", + }); + + Cu.exportFunction(this.AWSelectTheme.bind(this), window, { + defineAs: "AWSelectTheme", + }); + + Cu.exportFunction(this.AWEvaluateScreenTargeting.bind(this), window, { + defineAs: "AWEvaluateScreenTargeting", + }); + + Cu.exportFunction(this.AWSendEventTelemetry.bind(this), window, { + defineAs: "AWSendEventTelemetry", + }); + + Cu.exportFunction(this.AWSendToParent.bind(this), window, { + defineAs: "AWSendToParent", + }); + + Cu.exportFunction(this.AWWaitForMigrationClose.bind(this), window, { + defineAs: "AWWaitForMigrationClose", + }); + + Cu.exportFunction(this.AWFinish.bind(this), window, { + defineAs: "AWFinish", + }); + + Cu.exportFunction(this.AWEnsureLangPackInstalled.bind(this), window, { + defineAs: "AWEnsureLangPackInstalled", + }); + + Cu.exportFunction( + this.AWNegotiateLangPackForLanguageMismatch.bind(this), + window, + { + defineAs: "AWNegotiateLangPackForLanguageMismatch", + } + ); + + Cu.exportFunction(this.AWSetRequestedLocales.bind(this), window, { + defineAs: "AWSetRequestedLocales", + }); + + Cu.exportFunction(this.AWSendToDeviceEmailsSupported.bind(this), window, { + defineAs: "AWSendToDeviceEmailsSupported", + }); + + Cu.exportFunction(this.AWNewScreen.bind(this), window, { + defineAs: "AWNewScreen", + }); + } + + /** + * Wrap a promise so content can use Promise methods. + */ + wrapPromise(promise) { + return new this.contentWindow.Promise((resolve, reject) => + promise.then(resolve, reject) + ); + } + + /** + * Clones the result of the query into the content window. + */ + sendQueryAndCloneForContent(...sendQueryArgs) { + return this.wrapPromise( + (async () => { + return Cu.cloneInto( + await this.sendQuery(...sendQueryArgs), + this.contentWindow + ); + })() + ); + } + + AWSelectTheme(data) { + return this.wrapPromise( + this.sendQuery("AWPage:SELECT_THEME", data.toUpperCase()) + ); + } + + AWEvaluateScreenTargeting(data) { + return this.sendQueryAndCloneForContent( + "AWPage:EVALUATE_SCREEN_TARGETING", + data + ); + } + + AWAddScreenImpression(screen) { + return this.wrapPromise( + this.sendQuery("AWPage:ADD_SCREEN_IMPRESSION", screen) + ); + } + + /** + * Send initial data to page including experiment information + */ + async getAWContent() { + let attributionData = await this.sendQuery("AWPage:GET_ATTRIBUTION_DATA"); + + // Return to AMO gets returned early. + if (attributionData?.template) { + lazy.log.debug("Loading about:welcome with RTAMO attribution data"); + return Cu.cloneInto(attributionData, this.contentWindow); + } else if (attributionData?.ua) { + lazy.log.debug("Loading about:welcome with UA attribution"); + } + + let experimentMetadata = + lazy.ExperimentAPI.getExperimentMetaData({ + featureId: "aboutwelcome", + }) || {}; + + lazy.log.debug( + `Loading about:welcome with ${ + experimentMetadata?.slug ?? "no" + } experiment` + ); + + let featureConfig = lazy.NimbusFeatures.aboutwelcome.getAllVariables(); + featureConfig.needDefault = await this.sendQuery("AWPage:NEED_DEFAULT"); + featureConfig.needPin = await this.sendQuery("AWPage:DOES_APP_NEED_PIN"); + if (featureConfig.languageMismatchEnabled) { + featureConfig.appAndSystemLocaleInfo = await this.sendQuery( + "AWPage:GET_APP_AND_SYSTEM_LOCALE_INFO" + ); + } + + // FeatureConfig (from experiments) has higher precendence + // to defaults. But the `screens` property isn't defined we shouldn't + // override the default with `null` + let defaults = lazy.AboutWelcomeDefaults.getDefaults(); + + const content = await lazy.AboutWelcomeDefaults.prepareContentForReact({ + ...attributionData, + ...experimentMetadata, + ...defaults, + ...featureConfig, + screens: featureConfig.screens ?? defaults.screens, + backdrop: featureConfig.backdrop ?? defaults.backdrop, + }); + + return Cu.cloneInto(content, this.contentWindow); + } + + AWGetFeatureConfig() { + return this.wrapPromise(this.getAWContent()); + } + + AWGetFxAMetricsFlowURI() { + return this.wrapPromise(this.sendQuery("AWPage:FXA_METRICS_FLOW_URI")); + } + + AWGetSelectedTheme() { + return this.wrapPromise(getSelectedTheme(this)); + } + + /** + * Send Event Telemetry + * @param {object} eventData + */ + AWSendEventTelemetry(eventData) { + this.AWSendToParent("TELEMETRY_EVENT", { + ...eventData, + event_context: { + ...eventData.event_context, + }, + }); + } + + /** + * Send message that can be handled by AboutWelcomeParent.jsm + * @param {string} type + * @param {any=} data + * @returns {Promise} + */ + AWSendToParent(type, data) { + return this.sendQueryAndCloneForContent(`AWPage:${type}`, data); + } + + AWWaitForMigrationClose() { + return this.wrapPromise(this.sendQuery("AWPage:WAIT_FOR_MIGRATION_CLOSE")); + } + + AWFinish() { + this.contentWindow.location.href = "about:home"; + } + + AWEnsureLangPackInstalled(negotiated, screenContent) { + const content = Cu.cloneInto(screenContent, {}); + return this.wrapPromise( + this.sendQuery( + "AWPage:ENSURE_LANG_PACK_INSTALLED", + negotiated.langPack + ).then(() => { + const formatting = []; + const l10n = new Localization( + ["branding/brand.ftl", "browser/newtab/onboarding.ftl"], + false, + undefined, + // Use the system-ish then app then default locale. + [...negotiated.requestSystemLocales, "en-US"] + ); + + // Add the negotiated language name as args. + function addMessageArgsAndUseLangPack(obj) { + for (const value of Object.values(obj)) { + if (value?.string_id) { + value.args = { + ...value.args, + negotiatedLanguage: negotiated.langPackDisplayName, + }; + + // Expose fluent strings wanting lang pack as raw. + if (value.useLangPack) { + formatting.push( + l10n.formatValue(value.string_id, value.args).then(raw => { + delete value.string_id; + value.raw = raw; + }) + ); + } + } + } + } + addMessageArgsAndUseLangPack(content.languageSwitcher); + addMessageArgsAndUseLangPack(content); + return Promise.all(formatting).then(() => + Cu.cloneInto(content, this.contentWindow) + ); + }) + ); + } + + AWSetRequestedLocales(requestSystemLocales) { + return this.sendQueryAndCloneForContent( + "AWPage:SET_REQUESTED_LOCALES", + requestSystemLocales + ); + } + + AWNegotiateLangPackForLanguageMismatch(appAndSystemLocaleInfo) { + return this.sendQueryAndCloneForContent( + "AWPage:NEGOTIATE_LANGPACK", + appAndSystemLocaleInfo + ); + } + + AWSendToDeviceEmailsSupported() { + return this.wrapPromise( + this.sendQuery("AWPage:SEND_TO_DEVICE_EMAILS_SUPPORTED") + ); + } + + AWNewScreen(screenId) { + return this.wrapPromise(this.sendQuery("AWPage:NEW_SCREEN", screenId)); + } + + /** + * @param {{type: string, detail?: any}} event + * @override + */ + handleEvent(event) { + lazy.log.debug(`Received page event ${event.type}`); + } +} diff --git a/browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm b/browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm new file mode 100644 index 0000000000..2c9ff90eb2 --- /dev/null +++ b/browser/components/newtab/aboutwelcome/AboutWelcomeParent.jsm @@ -0,0 +1,266 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["AboutWelcomeParent"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", + FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs", + LangPackMatcher: "resource://gre/modules/LangPackMatcher.sys.mjs", + ShellService: "resource:///modules/ShellService.sys.mjs", + SpecialMessageActions: + "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AboutWelcomeTelemetry: + "resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm", + AboutWelcomeDefaults: + "resource://activity-stream/aboutwelcome/lib/AboutWelcomeDefaults.jsm", + AWScreenUtils: "resource://activity-stream/lib/AWScreenUtils.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "log", () => { + const { Logger } = ChromeUtils.importESModule( + "resource://messaging-system/lib/Logger.sys.mjs" + ); + return new Logger("AboutWelcomeParent"); +}); + +XPCOMUtils.defineLazyGetter( + lazy, + "Telemetry", + () => new lazy.AboutWelcomeTelemetry() +); + +const DID_SEE_ABOUT_WELCOME_PREF = "trailhead.firstrun.didSeeAboutWelcome"; +const AWTerminate = { + WINDOW_CLOSED: "welcome-window-closed", + TAB_CLOSED: "welcome-tab-closed", + APP_SHUT_DOWN: "app-shut-down", + ADDRESS_BAR_NAVIGATED: "address-bar-navigated", +}; +const LIGHT_WEIGHT_THEMES = { + AUTOMATIC: "default-theme@mozilla.org", + DARK: "firefox-compact-dark@mozilla.org", + LIGHT: "firefox-compact-light@mozilla.org", + ALPENGLOW: "firefox-alpenglow@mozilla.org", + "PLAYMAKER-SOFT": "playmaker-soft-colorway@mozilla.org", + "PLAYMAKER-BALANCED": "playmaker-balanced-colorway@mozilla.org", + "PLAYMAKER-BOLD": "playmaker-bold-colorway@mozilla.org", + "EXPRESSIONIST-SOFT": "expressionist-soft-colorway@mozilla.org", + "EXPRESSIONIST-BALANCED": "expressionist-balanced-colorway@mozilla.org", + "EXPRESSIONIST-BOLD": "expressionist-bold-colorway@mozilla.org", + "VISIONARY-SOFT": "visionary-soft-colorway@mozilla.org", + "VISIONARY-BALANCED": "visionary-balanced-colorway@mozilla.org", + "VISIONARY-BOLD": "visionary-bold-colorway@mozilla.org", + "ACTIVIST-SOFT": "activist-soft-colorway@mozilla.org", + "ACTIVIST-BALANCED": "activist-balanced-colorway@mozilla.org", + "ACTIVIST-BOLD": "activist-bold-colorway@mozilla.org", + "DREAMER-SOFT": "dreamer-soft-colorway@mozilla.org", + "DREAMER-BALANCED": "dreamer-balanced-colorway@mozilla.org", + "DREAMER-BOLD": "dreamer-bold-colorway@mozilla.org", + "INNOVATOR-SOFT": "innovator-soft-colorway@mozilla.org", + "INNOVATOR-BALANCED": "innovator-balanced-colorway@mozilla.org", + "INNOVATOR-BOLD": "innovator-bold-colorway@mozilla.org", +}; + +class AboutWelcomeObserver { + constructor() { + Services.obs.addObserver(this, "quit-application"); + + this.win = Services.focus.activeWindow; + if (!this.win) { + return; + } + + this.terminateReason = AWTerminate.ADDRESS_BAR_NAVIGATED; + + this.onWindowClose = () => { + this.terminateReason = AWTerminate.WINDOW_CLOSED; + }; + + this.onTabClose = () => { + this.terminateReason = AWTerminate.TAB_CLOSED; + }; + + this.win.addEventListener("TabClose", this.onTabClose, { once: true }); + this.win.addEventListener("unload", this.onWindowClose, { once: true }); + } + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "quit-application": + this.terminateReason = AWTerminate.APP_SHUT_DOWN; + break; + } + } + + // Added for testing + get AWTerminate() { + return AWTerminate; + } + + stop() { + lazy.log.debug(`Terminate reason is ${this.terminateReason}`); + Services.obs.removeObserver(this, "quit-application"); + if (!this.win) { + return; + } + this.win.removeEventListener("TabClose", this.onTabClose); + this.win.removeEventListener("unload", this.onWindowClose); + this.win = null; + } +} + +class AboutWelcomeParent extends JSWindowActorParent { + constructor() { + super(); + this.AboutWelcomeObserver = new AboutWelcomeObserver(this); + } + + // Static methods that calls into ShellService to check + // if Firefox is pinned or already default + static doesAppNeedPin() { + return lazy.ShellService.doesAppNeedPin(); + } + + static isDefaultBrowser() { + return lazy.ShellService.isDefaultBrowser(); + } + + didDestroy() { + if (this.AboutWelcomeObserver) { + this.AboutWelcomeObserver.stop(); + } + this.RegionHomeObserver?.stop(); + + lazy.Telemetry.sendTelemetry({ + event: "SESSION_END", + event_context: { + reason: this.AboutWelcomeObserver.terminateReason, + page: "about:welcome", + }, + message_id: this.AWMessageId, + }); + } + + /** + * Handle messages from AboutWelcomeChild.jsm + * + * @param {string} type + * @param {any=} data + * @param {Browser} the xul:browser rendering the page + */ + async onContentMessage(type, data, browser) { + lazy.log.debug(`Received content event: ${type}`); + switch (type) { + case "AWPage:SET_WELCOME_MESSAGE_SEEN": + this.AWMessageId = data; + try { + Services.prefs.setBoolPref(DID_SEE_ABOUT_WELCOME_PREF, true); + } catch (e) { + lazy.log.debug(`Fails to set ${DID_SEE_ABOUT_WELCOME_PREF}.`); + } + break; + case "AWPage:SPECIAL_ACTION": + return lazy.SpecialMessageActions.handleAction(data, browser); + case "AWPage:FXA_METRICS_FLOW_URI": + return lazy.FxAccounts.config.promiseMetricsFlowURI("aboutwelcome"); + case "AWPage:TELEMETRY_EVENT": + lazy.Telemetry.sendTelemetry(data); + break; + case "AWPage:GET_ATTRIBUTION_DATA": + let attributionData = + await lazy.AboutWelcomeDefaults.getAttributionContent(); + return attributionData; + case "AWPage:SELECT_THEME": + await lazy.BuiltInThemes.ensureBuiltInThemes(); + return lazy.AddonManager.getAddonByID(LIGHT_WEIGHT_THEMES[data]).then( + addon => addon.enable() + ); + case "AWPage:GET_SELECTED_THEME": + let themes = await lazy.AddonManager.getAddonsByTypes(["theme"]); + let activeTheme = themes.find(addon => addon.isActive); + // Store the current theme ID so user can restore their previous theme. + if (activeTheme?.id) { + LIGHT_WEIGHT_THEMES.AUTOMATIC = activeTheme.id; + } + // convert this to the short form name that the front end code + // expects + let themeShortName = Object.keys(LIGHT_WEIGHT_THEMES).find( + key => LIGHT_WEIGHT_THEMES[key] === activeTheme?.id + ); + return themeShortName?.toLowerCase(); + case "AWPage:DOES_APP_NEED_PIN": + return AboutWelcomeParent.doesAppNeedPin(); + case "AWPage:NEED_DEFAULT": + // Only need to set default if we're supposed to check and not default. + return ( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser") && + !AboutWelcomeParent.isDefaultBrowser() + ); + case "AWPage:WAIT_FOR_MIGRATION_CLOSE": + // Support multiples types of migration: 1) content modal 2) old + // migration modal 3) standalone content modal + return new Promise(resolve => { + const topics = [ + "MigrationWizard:Closed", + "MigrationWizard:Destroyed", + ]; + const observer = () => { + topics.forEach(t => Services.obs.removeObserver(observer, t)); + resolve(); + }; + topics.forEach(t => Services.obs.addObserver(observer, t)); + }); + case "AWPage:GET_APP_AND_SYSTEM_LOCALE_INFO": + return lazy.LangPackMatcher.getAppAndSystemLocaleInfo(); + case "AWPage:EVALUATE_SCREEN_TARGETING": + return lazy.AWScreenUtils.evaluateTargetingAndRemoveScreens(data); + case "AWPage:ADD_SCREEN_IMPRESSION": + return lazy.AWScreenUtils.addScreenImpression(data); + case "AWPage:NEGOTIATE_LANGPACK": + return lazy.LangPackMatcher.negotiateLangPackForLanguageMismatch(data); + case "AWPage:ENSURE_LANG_PACK_INSTALLED": + return lazy.LangPackMatcher.ensureLangPackInstalled(data); + case "AWPage:SET_REQUESTED_LOCALES": + return lazy.LangPackMatcher.setRequestedAppLocales(data); + case "AWPage:SEND_TO_DEVICE_EMAILS_SUPPORTED": { + return lazy.BrowserUtils.sendToDeviceEmailsSupported(); + } + default: + lazy.log.debug(`Unexpected event ${type} was not handled.`); + } + + return undefined; + } + + /** + * @param {{name: string, data?: any}} message + * @override + */ + receiveMessage(message) { + const { name, data } = message; + let browser; + + if (this.manager.rootFrameLoader) { + browser = this.manager.rootFrameLoader.ownerElement; + return this.onContentMessage(name, data, browser); + } + + lazy.log.warn(`Not handling ${name} because the browser doesn't exist.`); + return null; + } +} diff --git a/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js new file mode 100644 index 0000000000..fd52cc91e0 --- /dev/null +++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js @@ -0,0 +1,2321 @@ +/*! + * + * NOTE: This file is generated by webpack from aboutwelcome.jsx + * using the npm bundle task. + * + */ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ([ +/* 0 */, +/* 1 */ +/***/ ((module) => { + +module.exports = React; + +/***/ }), +/* 2 */ +/***/ ((module) => { + +module.exports = ReactDOM; + +/***/ }), +/* 3 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "AboutWelcomeUtils": () => (/* binding */ AboutWelcomeUtils), +/* harmony export */ "DEFAULT_RTAMO_CONTENT": () => (/* binding */ DEFAULT_RTAMO_CONTENT) +/* harmony export */ }); +var _document$querySelect; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +// If the container has a "page" data attribute, then this is +// a Spotlight modal or Feature Callout. Otherwise, this is +// about:welcome and we should return the current page. +const page = ((_document$querySelect = document.querySelector("#multi-stage-message-root.onboardingContainer[data-page]")) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.dataset.page) || document.location.href; +const AboutWelcomeUtils = { + handleUserAction(action) { + return window.AWSendToParent("SPECIAL_ACTION", action); + }, + + sendImpressionTelemetry(messageId, context) { + var _window$AWSendEventTe, _window; + + (_window$AWSendEventTe = (_window = window).AWSendEventTelemetry) === null || _window$AWSendEventTe === void 0 ? void 0 : _window$AWSendEventTe.call(_window, { + event: "IMPRESSION", + event_context: { ...context, + page + }, + message_id: messageId + }); + }, + + sendActionTelemetry(messageId, elementId, eventName = "CLICK_BUTTON") { + var _window$AWSendEventTe2, _window2; + + const ping = { + event: eventName, + event_context: { + source: elementId, + page + }, + message_id: messageId + }; + (_window$AWSendEventTe2 = (_window2 = window).AWSendEventTelemetry) === null || _window$AWSendEventTe2 === void 0 ? void 0 : _window$AWSendEventTe2.call(_window2, ping); + }, + + sendDismissTelemetry(messageId, elementId) { + // Don't send DISMISS telemetry in spotlight modals since they already send + // their own equivalent telemetry. + if (page !== "spotlight") { + this.sendActionTelemetry(messageId, elementId, "DISMISS"); + } + }, + + async fetchFlowParams(metricsFlowUri) { + let flowParams; + + try { + const response = await fetch(metricsFlowUri, { + credentials: "omit" + }); + + if (response.status === 200) { + const { + deviceId, + flowId, + flowBeginTime + } = await response.json(); + flowParams = { + deviceId, + flowId, + flowBeginTime + }; + } else { + console.error("Non-200 response", response); + } + } catch (e) { + flowParams = null; + } + + return flowParams; + }, + + sendEvent(type, detail) { + document.dispatchEvent(new CustomEvent(`AWPage:${type}`, { + bubbles: true, + detail + })); + } + +}; +const DEFAULT_RTAMO_CONTENT = { + template: "return_to_amo", + utm_term: "rtamo", + content: { + position: "split", + title: { + string_id: "mr1-return-to-amo-subtitle" + }, + has_noodles: false, + subtitle: { + string_id: "mr1-return-to-amo-addon-title" + }, + backdrop: "var(--mr-welcome-background-color) var(--mr-welcome-background-gradient)", + background: "url('chrome://activity-stream/content/data/content/assets/mr-rtamo-background-image.svg') no-repeat center", + progress_bar: true, + primary_button: { + label: { + string_id: "mr1-return-to-amo-add-extension-label" + }, + source_id: "ADD_EXTENSION_BUTTON", + action: { + type: "INSTALL_ADDON_FROM_URL", + data: { + url: null, + telemetrySource: "rtamo" + } + } + }, + secondary_button: { + label: { + string_id: "onboarding-not-now-button-label" + }, + source_id: "RTAMO_START_BROWSING_BUTTON", + action: { + type: "OPEN_AWESOME_BAR" + } + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label" + }, + source_id: "RTAMO_FXA_SIGNIN_BUTTON", + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab" + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true + } + } + } +}; + +/***/ }), +/* 4 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "MultiStageAboutWelcome": () => (/* binding */ MultiStageAboutWelcome), +/* harmony export */ "SecondaryCTA": () => (/* binding */ SecondaryCTA), +/* harmony export */ "StepsIndicator": () => (/* binding */ StepsIndicator), +/* harmony export */ "ProgressBar": () => (/* binding */ ProgressBar), +/* harmony export */ "WelcomeScreen": () => (/* binding */ WelcomeScreen) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); +/* harmony import */ var _MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6); +/* harmony import */ var _LanguageSwitcher__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(11); +/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(17); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + + + + // Amount of milliseconds for all transitions to complete (including delays). + +const TRANSITION_OUT_TIME = 1000; +const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH"; +const MultiStageAboutWelcome = props => { + let { + defaultScreens + } = props; + const didFilter = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(false); + const [didMount, setDidMount] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false); + const [screens, setScreens] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(defaultScreens); + const [index, setScreenIndex] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.startScreen); + const [previousOrder, setPreviousOrder] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.startScreen - 1); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + (async () => { + // If we want to load index from history state, we don't want to send impression yet + if (!didMount) { + return; + } // On about:welcome first load, screensVisited should be empty + + + let screensVisited = didFilter.current ? screens.slice(0, index) : []; + let upcomingScreens = defaultScreens.filter(s => !screensVisited.find(v => v.id === s.id)) // Filter out Language Mismatch screen from upcoming + // screens if screens set from useLanguageSwitcher hook + // has filtered language screen + .filter(upcomingScreen => !(!screens.find(s => s.id === LANGUAGE_MISMATCH_SCREEN_ID) && upcomingScreen.id === LANGUAGE_MISMATCH_SCREEN_ID)); + let filteredScreens = screensVisited.concat((await window.AWEvaluateScreenTargeting(upcomingScreens)) ?? upcomingScreens); // Use existing screen for the filtered screen to carry over any modification + // e.g. if AW_LANGUAGE_MISMATCH exists, use it from existing screens + + setScreens(filteredScreens.map(filtered => screens.find(s => s.id === filtered.id) ?? filtered)); + didFilter.current = true; + const screenInitials = filteredScreens.map(({ + id + }) => { + var _id$split$; + + return id === null || id === void 0 ? void 0 : (_id$split$ = id.split("_")[1]) === null || _id$split$ === void 0 ? void 0 : _id$split$[0]; + }).join(""); // Send impression ping when respective screen first renders + + filteredScreens.forEach((screen, order) => { + if (index === order) { + var _window$AWAddScreenIm, _window; + + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendImpressionTelemetry(`${props.message_id}_${order}_${screen.id}_${screenInitials}`); + (_window$AWAddScreenIm = (_window = window).AWAddScreenImpression) === null || _window$AWAddScreenIm === void 0 ? void 0 : _window$AWAddScreenIm.call(_window, screen); + } + }); // Remember that a new screen has loaded for browser navigation + + if (props.updateHistory && index > window.history.state) { + window.history.pushState(index, ""); + } // Remember the previous screen index so we can animate the transition + + + setPreviousOrder(index); + })(); + }, [index, didMount]); // eslint-disable-line react-hooks/exhaustive-deps + + const [flowParams, setFlowParams] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null); + const { + metricsFlowUri + } = props; + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + (async () => { + if (metricsFlowUri) { + setFlowParams(await _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.fetchFlowParams(metricsFlowUri)); + } + })(); + }, [metricsFlowUri]); // Allow "in" style to render to actually transition towards regular state, + // which also makes using browser back/forward navigation skip transitions. + + const [transition, setTransition] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.transitions ? "in" : ""); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + if (transition === "in") { + requestAnimationFrame(() => requestAnimationFrame(() => setTransition(""))); + } + }, [transition]); // Transition to next screen, opening about:home on last screen button CTA + + const handleTransition = () => { + // Only handle transitioning out from a screen once. + if (transition === "out") { + return; + } // Start transitioning things "out" immediately when moving forwards. + + + setTransition(props.transitions ? "out" : ""); // Actually move forwards after all transitions finish. + + setTimeout(() => { + if (index < screens.length - 1) { + setTransition(props.transitions ? "in" : ""); + setScreenIndex(prevState => prevState + 1); + } else { + window.AWFinish(); + } + }, props.transitions ? TRANSITION_OUT_TIME : 0); + }; + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + // When about:welcome loads (on refresh or pressing back button + // from about:home), ensure history state usEffect runs before + // useEffect hook that send impression telemetry + setDidMount(true); + + if (props.updateHistory) { + // Switch to the screen tracked in state (null for initial state) + // or last screen index if a user navigates by pressing back + // button from about:home + const handler = ({ + state + }) => { + if (transition === "out") { + return; + } + + setTransition(props.transitions ? "out" : ""); + setTimeout(() => { + setTransition(props.transitions ? "in" : ""); + setScreenIndex(Math.min(state, screens.length - 1)); + }, props.transitions ? TRANSITION_OUT_TIME : 0); + }; // Handle page load, e.g., going back to about:welcome from about:home + + + const { + state + } = window.history; + + if (state) { + setScreenIndex(Math.min(state, screens.length - 1)); + setPreviousOrder(Math.min(state, screens.length - 1)); + } // Watch for browser back/forward button navigation events + + + window.addEventListener("popstate", handler); + return () => window.removeEventListener("popstate", handler); + } + + return false; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Save the active multi select state containing array of checkbox ids + // used in handleAction to update MULTI_ACTION data + + const [activeMultiSelect, setActiveMultiSelect] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null); // Get the active theme so the rendering code can make it selected + // by default. + + const [activeTheme, setActiveTheme] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null); + const [initialTheme, setInitialTheme] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + (async () => { + let theme = await window.AWGetSelectedTheme(); + setInitialTheme(theme); + setActiveTheme(theme); + })(); + }, []); + const { + negotiatedLanguage, + langPackInstallPhase, + languageFilteredScreens + } = (0,_LanguageSwitcher__WEBPACK_IMPORTED_MODULE_4__.useLanguageSwitcher)(props.appAndSystemLocaleInfo, screens, index, setScreenIndex); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + setScreens(languageFilteredScreens); + }, [languageFilteredScreens]); + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `outer-wrapper onboardingContainer proton transition-${transition}`, + style: props.backdrop ? { + background: props.backdrop + } : {} + }, screens.map((screen, order) => { + const isFirstScreen = screen === screens[0]; + const isLastScreen = screen === screens[screens.length - 1]; + const totalNumberOfScreens = screens.length; + const isSingleScreen = totalNumberOfScreens === 1; + return index === order ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(WelcomeScreen, { + key: screen.id + order, + id: screen.id, + totalNumberOfScreens: totalNumberOfScreens, + isFirstScreen: isFirstScreen, + isLastScreen: isLastScreen, + isSingleScreen: isSingleScreen, + order: order, + previousOrder: previousOrder, + content: screen.content, + navigate: handleTransition, + messageId: `${props.message_id}_${order}_${screen.id}`, + UTMTerm: props.utm_term, + flowParams: flowParams, + activeTheme: activeTheme, + initialTheme: initialTheme, + setActiveTheme: setActiveTheme, + setInitialTheme: setInitialTheme, + activeMultiSelect: activeMultiSelect, + setActiveMultiSelect: setActiveMultiSelect, + autoAdvance: screen.auto_advance, + negotiatedLanguage: negotiatedLanguage, + langPackInstallPhase: langPackInstallPhase + }) : null; + }))); +}; +const SecondaryCTA = props => { + var _props$content$second; + + let targetElement = props.position ? `secondary_button_${props.position}` : `secondary_button`; + const buttonStyling = (_props$content$second = props.content.secondary_button) !== null && _props$content$second !== void 0 && _props$content$second.has_arrow_icon ? `secondary text-link arrow-icon` : `secondary text-link`; + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: props.position ? `secondary-cta ${props.position}` : "secondary-cta" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: props.content[targetElement].text + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: props.content[targetElement].label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: buttonStyling, + value: targetElement, + onClick: props.handleAction + }))); +}; +const StepsIndicator = props => { + let steps = []; + + for (let i = 0; i < props.totalNumberOfScreens; i++) { + let className = `${i === props.order ? "current" : ""} ${i < props.order ? "complete" : ""}`; + steps.push( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + key: i, + className: `indicator ${className}`, + role: "presentation" + })); + } + + return steps; +}; +const ProgressBar = ({ + step, + previousStep, + totalNumberOfScreens +}) => { + const [progress, setProgress] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(previousStep / totalNumberOfScreens); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + // We don't need to hook any dependencies because any time the step changes, + // the screen's entire DOM tree will be re-rendered. + setProgress(step / totalNumberOfScreens); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "indicator", + role: "presentation", + style: { + "--progress-bar-progress": `${progress * 100}%` + } + }); +}; +class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) { + constructor(props) { + super(props); + this.handleAction = this.handleAction.bind(this); + } + + handleOpenURL(action, flowParams, UTMTerm) { + let { + type, + data + } = action; + + if (type === "SHOW_FIREFOX_ACCOUNTS") { + let params = { ..._asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_5__.BASE_PARAMS, + utm_term: `${UTMTerm}-screen` + }; + + if (action.addFlowParams && flowParams) { + params = { ...params, + ...flowParams + }; + } + + data = { ...data, + extraParams: params + }; + } else if (type === "OPEN_URL") { + let url = new URL(data.args); + (0,_asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_5__.addUtmParams)(url, `${UTMTerm}-screen`); + + if (action.addFlowParams && flowParams) { + url.searchParams.append("device_id", flowParams.deviceId); + url.searchParams.append("flow_id", flowParams.flowId); + url.searchParams.append("flow_begin_time", flowParams.flowBeginTime); + } + + data = { ...data, + args: url.toString() + }; + } + + return _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction({ + type, + data + }); + } + + async handleAction(event) { + let { + props + } = this; + const value = event.currentTarget.value ?? event.currentTarget.getAttribute("value"); + const source = event.source || value; + let targetContent = props.content[value] || props.content.tiles || props.content.languageSwitcher; + + if (!(targetContent && targetContent.action)) { + return; + } // Send telemetry before waiting on actions + + + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, source, event.name); // Send additional telemetry if a messaging surface like feature callout is + // dismissed via the dismiss button. Other causes of dismissal will be + // handled separately by the messaging surface's own code. + + if (value === "dismiss_button" && !event.name) { + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendDismissTelemetry(props.messageId, source); + } + + let { + action + } = targetContent; + + if (action.collectSelect) { + // Populate MULTI_ACTION data actions property with selected checkbox actions from tiles data + action.data = { + actions: [] + }; + + for (const checkbox of ((_props$content = props.content) === null || _props$content === void 0 ? void 0 : (_props$content$tiles = _props$content.tiles) === null || _props$content$tiles === void 0 ? void 0 : _props$content$tiles.data) ?? []) { + var _props$content, _props$content$tiles; + + let checkboxAction; + + if (this.props.activeMultiSelect.includes(checkbox.id)) { + checkboxAction = checkbox.checkedAction ?? checkbox.action; + } else { + checkboxAction = checkbox.uncheckedAction; + } + + if (checkboxAction) { + action.data.actions.push(checkboxAction); + } + } // Send telemetry with selected checkbox ids + + + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, props.activeMultiSelect, "SELECT_CHECKBOX"); + } + + let actionResult; + + if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) { + actionResult = await this.handleOpenURL(action, props.flowParams, props.UTMTerm); + } else if (action.type) { + actionResult = await _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction(action); + + if (action.type === "FXA_SIGNIN_FLOW") { + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, actionResult ? "sign_in" : "sign_in_cancel", "FXA_SIGNIN_FLOW"); + } // Wait until migration closes to complete the action + + + const hasMigrate = a => { + var _a$data, _a$data$actions; + + return a.type === "SHOW_MIGRATION_WIZARD" || a.type === "MULTI_ACTION" && ((_a$data = a.data) === null || _a$data === void 0 ? void 0 : (_a$data$actions = _a$data.actions) === null || _a$data$actions === void 0 ? void 0 : _a$data$actions.some(hasMigrate)); + }; + + if (hasMigrate(action)) { + await window.AWWaitForMigrationClose(); + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close"); + } + } // 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.currentTarget.value : this.props.initialTheme || action.theme; + this.props.setActiveTheme(themeToUse); + window.AWSelectTheme(themeToUse); + } // If the action has persistActiveTheme: true, we set the initial theme to the currently active theme + // so that it can be reverted to in the event that the user navigates away from the screen + + + if (action.persistActiveTheme) { + this.props.setInitialTheme(this.props.activeTheme); + } // `navigate` and `dismiss` can be true/false/undefined, or they can be a + // string "actionResult" in which case we should use the actionResult + // (boolean resolved by handleUserAction) + + + const shouldDoBehavior = behavior => behavior === "actionResult" ? actionResult : behavior; + + if (shouldDoBehavior(action.navigate)) { + props.navigate(); + } + + if (shouldDoBehavior(action.dismiss)) { + window.AWFinish(); + } + } + + render() { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_3__.MultiStageProtonScreen, { + content: this.props.content, + id: this.props.id, + order: this.props.order, + previousOrder: this.props.previousOrder, + activeTheme: this.props.activeTheme, + activeMultiSelect: this.props.activeMultiSelect, + setActiveMultiSelect: this.props.setActiveMultiSelect, + totalNumberOfScreens: this.props.totalNumberOfScreens, + appAndSystemLocaleInfo: this.props.appAndSystemLocaleInfo, + negotiatedLanguage: this.props.negotiatedLanguage, + langPackInstallPhase: this.props.langPackInstallPhase, + handleAction: this.handleAction, + messageId: this.props.messageId, + isFirstScreen: this.props.isFirstScreen, + isLastScreen: this.props.isLastScreen, + isSingleScreen: this.props.isSingleScreen, + startsWithCorner: this.props.startsWithCorner, + autoAdvance: this.props.autoAdvance + }); + } + +} + +/***/ }), +/* 5 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Localized": () => (/* binding */ Localized) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const CONFIGURABLE_STYLES = ["color", "fontSize", "fontWeight", "letterSpacing", "lineHeight", "marginBlock", "marginInline", "paddingBlock", "paddingInline"]; +const ZAP_SIZE_THRESHOLD = 160; +/** + * Based on the .text prop, localizes an inner element if a string_id + * is provided, OR renders plain text, OR hides it if nothing is provided. + * Allows configuring of some styles including zap underline and color. + * + * Examples: + * + * Localized text + * ftl: + * title = Welcome + * jsx: + *

+ * output: + *

Welcome

+ * + * Unlocalized text + * jsx: + *

+ *

+ * output: + *

Welcome

+ */ + +const Localized = ({ + text, + children +}) => { + // Dynamically determine the size of the zap style. + const zapRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createRef(); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + const { + current + } = zapRef; + if (current) requestAnimationFrame(() => current === null || current === void 0 ? void 0 : current.classList.replace("short", current.getBoundingClientRect().width > ZAP_SIZE_THRESHOLD ? "long" : "short")); + }); // Skip rendering of children with no text. + + if (!text) { + return null; + } // Allow augmenting existing child container properties. + + + const props = { + children: [], + className: "", + style: {}, + ...(children === null || children === void 0 ? void 0 : children.props) + }; // Support nested Localized by starting with their children. + + const textNodes = Array.isArray(props.children) ? props.children : [props.children]; // Pick desired fluent or raw/plain text to render. + + if (text.string_id) { + // Set the key so React knows not to reuse when switching to plain text. + props.key = text.string_id; + props["data-l10n-id"] = text.string_id; + if (text.args) props["data-l10n-args"] = JSON.stringify(text.args); + } else if (text.raw) { + textNodes.push(text.raw); + } else if (typeof text === "string") { + textNodes.push(text); + } // Add zap style and content in a way that allows fluent to insert too. + + + if (text.zap) { + props.className += " welcomeZap"; + textNodes.push( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { + className: "short zap", + "data-l10n-name": "zap", + ref: zapRef + }, text.zap)); + } + + if (text.aria_label) { + props["aria-label"] = text.aria_label; + } // Apply certain configurable styles. + + + CONFIGURABLE_STYLES.forEach(style => { + if (text[style] !== undefined) props.style[style] = text[style]; + }); + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().cloneElement( // Provide a default container for the text if necessary. + children ?? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null), props, // Conditionally pass in as void elements can't accept empty array. + textNodes.length ? textNodes : null); +}; + +/***/ }), +/* 6 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "MultiStageProtonScreen": () => (/* binding */ MultiStageProtonScreen), +/* harmony export */ "ProtonScreenActionButtons": () => (/* binding */ ProtonScreenActionButtons), +/* harmony export */ "ProtonScreen": () => (/* binding */ ProtonScreen) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* harmony import */ var _MRColorways__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7); +/* harmony import */ var _MobileDownloads__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8); +/* harmony import */ var _MultiSelect__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9); +/* harmony import */ var _Themes__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(10); +/* harmony import */ var _MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(4); +/* harmony import */ var _LanguageSwitcher__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(11); +/* harmony import */ var _CTAParagraph__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(12); +/* harmony import */ var _HeroImage__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(13); +/* harmony import */ var _OnboardingVideo__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(14); +/* harmony import */ var _AdditionalCTA__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(15); +/* harmony import */ var _EmbeddedMigrationWizard__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(16); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + + + + + + + + + + + +const MultiStageProtonScreen = props => { + const { + autoAdvance, + handleAction, + order + } = props; + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + if (autoAdvance) { + const timer = setTimeout(() => { + handleAction({ + currentTarget: { + value: autoAdvance + }, + name: "AUTO_ADVANCE" + }); + }, 20000); + return () => clearTimeout(timer); + } + + return () => {}; + }, [autoAdvance, handleAction, order]); + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(ProtonScreen, { + content: props.content, + id: props.id, + order: props.order, + activeTheme: props.activeTheme, + activeMultiSelect: props.activeMultiSelect, + setActiveMultiSelect: props.setActiveMultiSelect, + totalNumberOfScreens: props.totalNumberOfScreens, + handleAction: props.handleAction, + isFirstScreen: props.isFirstScreen, + isLastScreen: props.isLastScreen, + isSingleScreen: props.isSingleScreen, + previousOrder: props.previousOrder, + autoAdvance: props.autoAdvance, + isRtamo: props.isRtamo, + addonName: props.addonName, + isTheme: props.isTheme, + iconURL: props.iconURL, + messageId: props.messageId, + negotiatedLanguage: props.negotiatedLanguage, + langPackInstallPhase: props.langPackInstallPhase + }); +}; +const ProtonScreenActionButtons = props => { + var _content$checkbox, _content$additional_b, _content$primary_butt, _content$primary_butt2; + + const { + content, + addonName + } = props; + const defaultValue = (_content$checkbox = content.checkbox) === null || _content$checkbox === void 0 ? void 0 : _content$checkbox.defaultValue; + const [isChecked, setIsChecked] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(defaultValue || false); + + if (!content.primary_button && !content.secondary_button && !content.additional_button) { + return null; + } + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `action-buttons ${content.additional_button ? "additional-cta-container" : ""}`, + flow: (_content$additional_b = content.additional_button) === null || _content$additional_b === void 0 ? void 0 : _content$additional_b.flow + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: (_content$primary_butt = content.primary_button) === null || _content$primary_butt === void 0 ? void 0 : _content$primary_butt.label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: "primary" // Whether or not the checkbox is checked determines which action + // should be handled. By setting value here, we indicate to + // this.handleAction() where in the content tree it should take + // the action to execute from. + , + value: isChecked ? "checkbox" : "primary_button", + disabled: ((_content$primary_butt2 = content.primary_button) === null || _content$primary_butt2 === void 0 ? void 0 : _content$primary_butt2.disabled) === true, + onClick: props.handleAction, + "data-l10n-args": addonName ? JSON.stringify({ + "addon-name": addonName + }) : "" + })), content.additional_button ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_AdditionalCTA__WEBPACK_IMPORTED_MODULE_11__.AdditionalCTA, { + content: content, + handleAction: props.handleAction + }) : null, content.checkbox ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "checkbox-container" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { + type: "checkbox", + id: "action-checkbox", + checked: isChecked, + onChange: () => { + setIsChecked(!isChecked); + } + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.checkbox.label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { + htmlFor: "action-checkbox" + }))) : null, content.secondary_button ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_6__.SecondaryCTA, { + content: content, + handleAction: props.handleAction + }) : null); +}; +class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) { + componentDidMount() { + this.mainContentHeader.focus(); + } + + getScreenClassName(isFirstScreen, isLastScreen, includeNoodles, isVideoOnboarding) { + const screenClass = `screen-${this.props.order % 2 !== 0 ? 1 : 2}`; + if (isVideoOnboarding) return "with-video"; + return `${isFirstScreen ? `dialog-initial` : ``} ${isLastScreen ? `dialog-last` : ``} ${includeNoodles ? `with-noodles` : ``} ${screenClass}`; + } + + renderLogo({ + imageURL = "chrome://branding/content/about-logo.svg", + darkModeImageURL, + reducedMotionImageURL, + darkModeReducedMotionImageURL, + alt = "", + height + }) { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("picture", { + className: "logo-container" + }, darkModeReducedMotionImageURL ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("source", { + srcSet: darkModeReducedMotionImageURL, + media: "(prefers-color-scheme: dark) and (prefers-reduced-motion: reduce)" + }) : null, darkModeImageURL ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("source", { + srcSet: darkModeImageURL, + media: "(prefers-color-scheme: dark)" + }) : null, reducedMotionImageURL ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("source", { + srcSet: reducedMotionImageURL, + media: "(prefers-reduced-motion: reduce)" + }) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { + className: "brand-logo", + style: { + height + }, + src: imageURL, + alt: alt, + role: alt ? null : "presentation" + })); + } + + renderContentTiles() { + const { + content + } = this.props; + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, content.tiles && content.tiles.type === "colorway" && content.tiles.colorways ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MRColorways__WEBPACK_IMPORTED_MODULE_2__.Colorways, { + content: content, + activeTheme: this.props.activeTheme, + handleAction: this.props.handleAction + }) : null, content.tiles && content.tiles.type === "theme" && content.tiles.data ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_Themes__WEBPACK_IMPORTED_MODULE_5__.Themes, { + content: content, + activeTheme: this.props.activeTheme, + handleAction: this.props.handleAction + }) : null, content.tiles && content.tiles.type === "mobile_downloads" && content.tiles.data ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MobileDownloads__WEBPACK_IMPORTED_MODULE_3__.MobileDownloads, { + data: content.tiles.data, + handleAction: this.props.handleAction + }) : null, content.tiles && content.tiles.type === "multiselect" && content.tiles.data ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiSelect__WEBPACK_IMPORTED_MODULE_4__.MultiSelect, { + content: content, + activeMultiSelect: this.props.activeMultiSelect, + setActiveMultiSelect: this.props.setActiveMultiSelect, + handleAction: this.props.handleAction + }) : null, content.tiles && content.tiles.type === "migration-wizard" ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedMigrationWizard__WEBPACK_IMPORTED_MODULE_12__.EmbeddedMigrationWizard, { + handleAction: this.props.handleAction + }) : null); + } + + renderNoodles() { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "noodle orange-L" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "noodle purple-C" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "noodle solid-L" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "noodle outline-L" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "noodle yellow-circle" + })); + } + + renderLanguageSwitcher() { + return this.props.content.languageSwitcher ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_LanguageSwitcher__WEBPACK_IMPORTED_MODULE_7__.LanguageSwitcher, { + content: this.props.content, + handleAction: this.props.handleAction, + negotiatedLanguage: this.props.negotiatedLanguage, + langPackInstallPhase: this.props.langPackInstallPhase, + messageId: this.props.messageId + }) : null; + } + + renderDismissButton() { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: "dismiss-button", + onClick: this.props.handleAction, + value: "dismiss_button", + "data-l10n-id": "spotlight-dialog-close-button" + }); + } + + renderStepsIndicator() { + const currentStep = (this.props.order ?? 0) + 1; + const previousStep = (this.props.previousOrder ?? -1) + 1; + const { + content, + totalNumberOfScreens: total + } = this.props; + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + id: "steps", + className: `steps${content.progress_bar ? " progress-bar" : ""}`, + "data-l10n-id": "onboarding-welcome-steps-indicator-label", + "data-l10n-args": JSON.stringify({ + current: currentStep, + total: total ?? 0 + }), + "data-l10n-attrs": "aria-label", + role: "progressbar", + "aria-valuenow": currentStep, + "aria-valuemin": 1, + "aria-valuemax": total + }, content.progress_bar ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_6__.ProgressBar, { + step: currentStep, + previousStep: previousStep, + totalNumberOfScreens: total + }) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_6__.StepsIndicator, { + order: this.props.order, + totalNumberOfScreens: total + })); + } + + renderSecondarySection(content) { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "section-secondary", + style: content.background ? { + background: content.background, + "--mr-secondary-background-position-y": content.split_narrow_bkg_position + } : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.image_alt_text + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "sr-only image-alt", + role: "img" + })), content.hero_image ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_HeroImage__WEBPACK_IMPORTED_MODULE_9__.HeroImage, { + url: content.hero_image.url + }) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "message-text" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "spacer-top" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.hero_text + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "spacer-bottom" + })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.help_text + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { + className: "attrib-text" + })))); + } + + render() { + var _content$tiles, _this$props$appAndSys, _this$props$messageId; + + const { + autoAdvance, + content, + isRtamo, + isTheme, + isFirstScreen, + isLastScreen, + isSingleScreen + } = this.props; + const includeNoodles = content.has_noodles; // The default screen position is "center" + + const isCenterPosition = content.position === "center" || !content.position; + const hideStepsIndicator = autoAdvance || (content === null || content === void 0 ? void 0 : content.video_container) || isSingleScreen; + const textColorClass = content.text_color ? `${content.text_color}-text` : ""; // Assign proton screen style 'screen-1' or 'screen-2' to centered screens + // by checking if screen order is even or odd. + + const screenClassName = isCenterPosition ? this.getScreenClassName(isFirstScreen, isLastScreen, includeNoodles, content === null || content === void 0 ? void 0 : content.video_container) : ""; + const isEmbeddedMigration = ((_content$tiles = content.tiles) === null || _content$tiles === void 0 ? void 0 : _content$tiles.type) === "migration-wizard"; + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("main", { + className: `screen ${this.props.id || ""} + ${screenClassName} ${textColorClass}`, + role: "alertdialog", + pos: content.position || "center", + tabIndex: "-1", + "aria-labelledby": "mainContentHeader", + ref: input => { + this.mainContentHeader = input; + } + }, isCenterPosition ? null : this.renderSecondarySection(content), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `section-main ${isEmbeddedMigration ? "embedded-migration" : ""}`, + role: "document" + }, content.secondary_button_top ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_6__.SecondaryCTA, { + content: content, + handleAction: this.props.handleAction, + position: "top" + }) : null, includeNoodles ? this.renderNoodles() : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `main-content ${hideStepsIndicator ? "no-steps" : ""}`, + style: content.background && isCenterPosition ? { + background: content.background + } : {} + }, content.logo ? this.renderLogo(content.logo) : null, isRtamo ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "rtamo-icon" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { + className: `${isTheme ? "rtamo-theme-icon" : "brand-logo"}`, + src: this.props.iconURL, + role: "presentation", + alt: "" + })) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "main-content-inner" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `welcome-text ${content.title_style || ""}` + }, content.title ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.title + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", { + id: "mainContentHeader" + })) : null, content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.subtitle + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h2", { + "data-l10n-args": JSON.stringify({ + "addon-name": this.props.addonName, + ...((_this$props$appAndSys = this.props.appAndSystemLocaleInfo) === null || _this$props$appAndSys === void 0 ? void 0 : _this$props$appAndSys.displayNames) + }), + "aria-flowto": (_this$props$messageId = this.props.messageId) !== null && _this$props$messageId !== void 0 && _this$props$messageId.includes("FEATURE_TOUR") ? "steps" : "" + })) : null, content.cta_paragraph ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_CTAParagraph__WEBPACK_IMPORTED_MODULE_8__.CTAParagraph, { + content: content.cta_paragraph, + handleAction: this.props.handleAction + }) : null), content.video_container ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_OnboardingVideo__WEBPACK_IMPORTED_MODULE_10__.OnboardingVideo, { + content: content.video_container, + handleAction: this.props.handleAction + }) : null, this.renderContentTiles(), this.renderLanguageSwitcher(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(ProtonScreenActionButtons, { + content: content, + addonName: this.props.addonName, + handleAction: this.props.handleAction + })), !hideStepsIndicator ? this.renderStepsIndicator() : null), content.dismiss_button ? this.renderDismissButton() : null)); + } + +} + +/***/ }), +/* 7 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "ColorwayDescription": () => (/* binding */ ColorwayDescription), +/* harmony export */ "computeColorWay": () => (/* binding */ computeColorWay), +/* harmony export */ "computeVariationIndex": () => (/* binding */ computeVariationIndex), +/* harmony export */ "Colorways": () => (/* binding */ Colorways) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +const ColorwayDescription = props => { + const { + colorway + } = props; + + if (!colorway) { + return null; + } + + const { + label, + description + } = colorway; + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: description + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "colorway-text", + "data-l10n-args": JSON.stringify({ + colorwayName: label + }) + })); +}; // Return colorway as "default" for default theme variations Automatic, Light, Dark, +// Alpenglow theme and legacy colorways which is not supported in Colorway picker. +// For themes other then default, theme names exist in +// format colorway-variationId inside LIGHT_WEIGHT_THEMES in AboutWelcomeParent + +function computeColorWay(themeName, systemVariations) { + return !themeName || themeName === "alpenglow" || systemVariations.includes(themeName) ? "default" : themeName.split("-")[0]; +} // Set variationIndex based off activetheme value e.g. 'light', 'expressionist-soft' + +function computeVariationIndex(themeName, systemVariations, variations, defaultVariationIndex) { + // Check if themeName is in systemVariations, if yes choose variationIndex by themeName + let index = systemVariations.findIndex(theme => theme === themeName); + + if (index >= 0) { + return index; + } // If themeName is one of the colorways, select variation index from colorways + + + let variation = themeName === null || themeName === void 0 ? void 0 : themeName.split("-")[1]; + index = variations.findIndex(element => element === variation); + + if (index >= 0) { + return index; + } + + return defaultVariationIndex; +} +function Colorways(props) { + let { + colorways, + darkVariation, + defaultVariationIndex, + systemVariations, + variations + } = props.content.tiles; + let hasReverted = false; // Active theme id from JSON e.g. "expressionist" + + const activeId = computeColorWay(props.activeTheme, systemVariations); + const [colorwayId, setState] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(activeId); + const [variationIndex, setVariationIndex] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(defaultVariationIndex); + + function revertToDefaultTheme() { + if (hasReverted) return; // Spoofing an event with current target value of "navigate_away" + // helps the handleAction method to read the colorways theme as "revert" + // which causes the initial theme to be activated. + // The "navigate_away" action is set in content in the colorways screen JSON config. + // Any value in the JSON for theme will work, provided it is not ``. + + const event = { + currentTarget: { + value: "navigate_away" + } + }; + props.handleAction(event); + hasReverted = true; + } // Revert to default theme if the user navigates away from the page or spotlight modal + // before clicking on the primary button to officially set theme. + + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + addEventListener("beforeunload", revertToDefaultTheme); + addEventListener("pagehide", revertToDefaultTheme); + return () => { + removeEventListener("beforeunload", revertToDefaultTheme); + removeEventListener("pagehide", revertToDefaultTheme); + }; + }); // Update state any time activeTheme changes. + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + setState(computeColorWay(props.activeTheme, systemVariations)); + setVariationIndex(computeVariationIndex(props.activeTheme, systemVariations, variations, defaultVariationIndex)); // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.activeTheme]); //select a random colorway + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + //We don't want the default theme to be selected + const randomIndex = Math.floor(Math.random() * (colorways.length - 1)) + 1; + const randomColorwayId = colorways[randomIndex].id; // Change the variation to be the dark variation if configured and dark. + // Additional colorway changes will remain dark while system is unchanged. + + if (darkVariation !== undefined && window.matchMedia("(prefers-color-scheme: dark)").matches) { + variations[variationIndex] = variations[darkVariation]; + } + + const value = `${randomColorwayId}-${variations[variationIndex]}`; + props.handleAction({ + currentTarget: { + value + } + }); // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "tiles-theme-container" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("fieldset", { + className: "tiles-theme-section" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: props.content.subtitle + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("legend", { + className: "sr-only" + })), colorways.map(({ + id, + label, + tooltip + }) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + key: id + label, + text: typeof tooltip === "object" ? tooltip : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { + className: "theme", + title: label, + "data-l10n-args": JSON.stringify({ + colorwayName: label + }) + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: typeof tooltip === "object" ? tooltip : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { + className: "sr-only colorway label", + id: `${id}-label`, + "data-l10n-args": JSON.stringify({ + colorwayName: tooltip + }) + })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: typeof label === "object" ? label : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { + type: "radio", + "data-colorway": id, + name: "theme", + value: id === "default" ? systemVariations[variationIndex] : `${id}-${variations[variationIndex]}`, + checked: colorwayId === id, + className: "sr-only input", + onClick: props.handleAction, + "data-l10n-args": JSON.stringify({ + colorwayName: label + }), + "aria-labelledby": `${id}-label` + })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `icon colorway ${colorwayId === id ? "selected" : ""} ${id}` + })))))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(ColorwayDescription, { + colorway: colorways.find(colorway => colorway.id === activeId) + })); +} + +/***/ }), +/* 8 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "MarketplaceButtons": () => (/* binding */ MarketplaceButtons), +/* harmony export */ "MobileDownloads": () => (/* binding */ MobileDownloads) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +const MarketplaceButtons = props => { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("ul", { + className: "mobile-download-buttons" + }, props.buttons.includes("ios") ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("li", { + className: "ios" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + "data-l10n-id": "spotlight-ios-marketplace-button", + value: "ios", + onClick: props.handleAction + })) : null, props.buttons.includes("android") ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("li", { + className: "android" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + "data-l10n-id": "spotlight-android-marketplace-button", + value: "android", + onClick: props.handleAction + })) : null); +}; +const MobileDownloads = props => { + const { + QR_code: QRCode + } = props.data; + const showEmailLink = props.data.email && window.AWSendToDeviceEmailsSupported(); + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "mobile-downloads" + }, QRCode ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { + "data-l10n-id": QRCode.alt_text.string_id ? QRCode.alt_text.string_id : null, + className: "qr-code-image", + alt: typeof QRCode.alt_text === "string" ? QRCode.alt_text : "", + src: QRCode.image_url + }) : null, showEmailLink ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: props.data.email.link_text + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: "email-link", + value: "email_link", + onClick: props.handleAction + }))) : null, props.data.marketplace_buttons ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(MarketplaceButtons, { + buttons: props.data.marketplace_buttons, + handleAction: props.handleAction + }) : null); +}; + +/***/ }), +/* 9 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "MultiSelect": () => (/* binding */ MultiSelect) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +const MultiSelect = props => { + let handleChange = event => { + if (event.currentTarget.checked) { + props.setActiveMultiSelect([...props.activeMultiSelect, event.currentTarget.value]); + } else { + props.setActiveMultiSelect(props.activeMultiSelect.filter(id => id !== event.currentTarget.value)); + } + }; + + let { + data + } = props.content.tiles; // When screen renders for first time, update state + // with checkbox ids that has defaultvalue true + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + if (!props.activeMultiSelect) { + props.setActiveMultiSelect(data.map(item => item.defaultValue && item.id).filter(item => !!item)); + } // eslint-disable-next-line react-hooks/exhaustive-deps + + }, []); + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "multi-select-container" + }, props.content.tiles.data.map(({ + label, + id + }) => { + var _props$activeMultiSel; + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + key: id + label, + className: "checkbox-container multi-select-item" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { + type: "checkbox", + id: id, + value: id, + checked: (_props$activeMultiSel = props.activeMultiSelect) === null || _props$activeMultiSel === void 0 ? void 0 : _props$activeMultiSel.includes(id), + onChange: handleChange + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { + htmlFor: id + }))); + })); +}; + +/***/ }), +/* 10 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Themes": () => (/* binding */ Themes) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +const Themes = props => { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "tiles-theme-container" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("fieldset", { + className: "tiles-theme-section" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: props.content.subtitle + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("legend", { + className: "sr-only" + })), props.content.tiles.data.map(({ + theme, + label, + tooltip, + description + }) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + key: theme + label, + text: typeof tooltip === "object" ? tooltip : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { + className: "theme", + title: theme + label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: typeof description === "object" ? description : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { + type: "radio", + value: theme, + name: "theme", + checked: theme === props.activeTheme, + className: "sr-only input", + onClick: props.handleAction + })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: `icon ${theme === props.activeTheme ? " selected" : ""} ${theme}` + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "text" + })))))))); +}; + +/***/ }), +/* 11 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "useLanguageSwitcher": () => (/* binding */ useLanguageSwitcher), +/* harmony export */ "LanguageSwitcher": () => (/* binding */ LanguageSwitcher) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + +/** + * The language switcher implements a hook that should be placed at a higher level + * than the actual language switcher component, as it needs to preemptively fetch + * and install langpacks for the user if there is a language mismatch screen. + */ + +function useLanguageSwitcher(appAndSystemLocaleInfo, screens, screenIndex, setScreenIndex) { + const languageMismatchScreenIndex = screens.findIndex(({ + id + }) => id === "AW_LANGUAGE_MISMATCH"); + const screen = screens[languageMismatchScreenIndex]; // Ensure fluent messages have the negotiatedLanguage args set, as they are rendered + // before the negotiatedLanguage is known. If the arg isn't present then Firefox will + // crash in development mode. + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + var _screen$content; + + if (screen !== null && screen !== void 0 && (_screen$content = screen.content) !== null && _screen$content !== void 0 && _screen$content.languageSwitcher) { + for (const text of Object.values(screen.content.languageSwitcher)) { + if (text !== null && text !== void 0 && text.args && text.args.negotiatedLanguage === undefined) { + text.args.negotiatedLanguage = ""; + } + } + } + }, [screen]); // If there is a mismatch, then Firefox can negotiate a better langpack to offer + // the user. + + const [negotiatedLanguage, setNegotiatedLanguage] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function getNegotiatedLanguage() { + if (!appAndSystemLocaleInfo) { + return; + } + + if (appAndSystemLocaleInfo.matchType !== "language-mismatch") { + // There is no language mismatch, so there is no need to negotiate a langpack. + return; + } + + (async () => { + const { + langPack, + langPackDisplayName + } = await window.AWNegotiateLangPackForLanguageMismatch(appAndSystemLocaleInfo); + + if (langPack) { + setNegotiatedLanguage({ + langPackDisplayName, + appDisplayName: appAndSystemLocaleInfo.displayNames.appLanguage, + langPack, + requestSystemLocales: [langPack.target_locale, appAndSystemLocaleInfo.appLocaleRaw], + originalAppLocales: [appAndSystemLocaleInfo.appLocaleRaw] + }); + } else { + setNegotiatedLanguage({ + langPackDisplayName: null, + appDisplayName: null, + langPack: null, + requestSystemLocales: null + }); + } + })(); + }, [appAndSystemLocaleInfo]); + /** + * @type { + * "before-installation" + * | "installing" + * | "installed" + * | "installation-error" + * | "none-available" + * } + */ + + const [langPackInstallPhase, setLangPackInstallPhase] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)("before-installation"); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function ensureLangPackInstalled() { + if (!negotiatedLanguage) { + // There are no negotiated languages to download yet. + return; + } + + setLangPackInstallPhase("installing"); + window.AWEnsureLangPackInstalled(negotiatedLanguage, screen === null || screen === void 0 ? void 0 : screen.content).then(content => { + // Update screen content with strings that might have changed. + screen.content = content; + setLangPackInstallPhase("installed"); + }, error => { + console.error(error); + setLangPackInstallPhase("installation-error"); + }); + }, [negotiatedLanguage]); + const [languageFilteredScreens, setLanguageFilteredScreens] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(screens); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function filterScreen() { + // Remove the language screen if it exists (already removed for no live + // reload) and we either don't-need-to or can't switch. + if (screen && ((appAndSystemLocaleInfo === null || appAndSystemLocaleInfo === void 0 ? void 0 : appAndSystemLocaleInfo.matchType) !== "language-mismatch" || (negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.langPack) === null)) { + if (screenIndex > languageMismatchScreenIndex) { + setScreenIndex(screenIndex - 1); + } + + setLanguageFilteredScreens(screens.filter(s => s.id !== "AW_LANGUAGE_MISMATCH")); + } else { + setLanguageFilteredScreens(screens); + } + }, [screens, negotiatedLanguage]); + return { + negotiatedLanguage, + langPackInstallPhase, + languageFilteredScreens + }; +} +/** + * The language switcher is a separate component as it needs to perform some asynchronous + * network actions such as retrieving the list of langpacks available, and downloading + * a new langpack. On a fast connection, this won't be noticeable, but on slow or unreliable + * internet this may fail for a user. + */ + +function LanguageSwitcher(props) { + const { + content, + handleAction, + negotiatedLanguage, + langPackInstallPhase, + messageId + } = props; + const [isAwaitingLangpack, setIsAwaitingLangpack] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false); // Determine the status of the langpack installation. + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + if (isAwaitingLangpack && langPackInstallPhase !== "installing") { + window.AWSetRequestedLocales(negotiatedLanguage.requestSystemLocales); + requestAnimationFrame(() => { + handleAction( // Simulate the click event. + { + currentTarget: { + value: "download_complete" + } + }); + }); + } + }, [isAwaitingLangpack, langPackInstallPhase]); + let showWaitingScreen = false; + let showPreloadingScreen = false; + let showReadyScreen = false; + + if (isAwaitingLangpack && langPackInstallPhase !== "installed") { + showWaitingScreen = true; + } else if (langPackInstallPhase === "before-installation") { + showPreloadingScreen = true; + } else { + showReadyScreen = true; + } // Use {display: "none"} rather than if statements to prevent layout thrashing with + // the localized text elements rendering as blank, then filling in the text. + + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "action-buttons language-switcher-container" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + style: { + display: showPreloadingScreen ? "block" : "none" + } + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: "primary", + value: "primary_button", + disabled: true, + type: "button" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { + className: "language-loader", + src: "chrome://browser/skin/tabbrowser/tab-connecting.png", + alt: "" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.languageSwitcher.waiting + })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "secondary-cta" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.languageSwitcher.skip + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + value: "decline_waiting", + type: "button", + className: "secondary text-link arrow-icon", + onClick: handleAction + })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + style: { + display: showWaitingScreen ? "block" : "none" + } + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: "primary", + value: "primary_button", + disabled: true, + type: "button" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { + className: "language-loader", + src: "chrome://browser/skin/tabbrowser/tab-connecting.png", + alt: "" + }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.languageSwitcher.downloading + })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "secondary-cta" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.languageSwitcher.cancel + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + type: "button", + className: "secondary text-link", + onClick: () => { + setIsAwaitingLangpack(false); + handleAction({ + currentTarget: { + value: "cancel_waiting" + } + }); + } + })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + style: { + display: showReadyScreen ? "block" : "none" + } + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: "primary", + value: "primary_button", + onClick: () => { + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(messageId, "download_langpack"); + setIsAwaitingLangpack(true); + } + }, content.languageSwitcher.switch ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.languageSwitcher.switch + }) : // This is the localized name from the Intl.DisplayNames API. + negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.langPackDisplayName)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + type: "button", + className: "primary", + value: "decline", + onClick: event => { + window.AWSetRequestedLocales(negotiatedLanguage.originalAppLocales); + handleAction(event); + } + }, content.languageSwitcher.continue ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.languageSwitcher.continue + }) : // This is the localized name from the Intl.DisplayNames API. + negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.appDisplayName)))); +} + +/***/ }), +/* 12 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "CTAParagraph": () => (/* binding */ CTAParagraph) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +const CTAParagraph = props => { + const { + content, + handleAction + } = props; + + if (!(content !== null && content !== void 0 && content.text)) { + return null; + } + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h2", { + className: "cta-paragraph" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: content.text + }, content.text.string_name && typeof handleAction === "function" ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { + "data-l10n-id": content.text.string_id, + onClick: handleAction, + onKeyUp: event => ["Enter", " "].includes(event.key) ? handleAction(event) : null, + value: "cta_paragraph", + role: "button", + tabIndex: "0" + }, " ", /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { + role: "button", + tabIndex: "0", + "data-l10n-name": content.text.string_name + }, " ")) : null)); +}; + +/***/ }), +/* 13 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "HeroImage": () => (/* binding */ HeroImage) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const HeroImage = props => { + const { + height, + url, + alt + } = props; + + if (!url) { + return null; + } + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "hero-image" + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { + style: height ? { + height + } : null, + src: url, + alt: alt || "", + role: alt ? null : "presentation" + })); +}; + +/***/ }), +/* 14 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "OnboardingVideo": () => (/* binding */ OnboardingVideo) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const OnboardingVideo = props => { + const vidUrl = props.content.video_url; + const autoplay = props.content.autoPlay; + + const handleVideoAction = event => { + props.handleAction({ + currentTarget: { + value: event + } + }); + }; + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("video", { + // eslint-disable-line jsx-a11y/media-has-caption + controls: true, + autoPlay: autoplay, + src: vidUrl, + width: "604px", + height: "340px", + onPlay: () => handleVideoAction("video_start"), + onEnded: () => handleVideoAction("video_end") + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("source", { + src: vidUrl + }))); +}; + +/***/ }), +/* 15 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "AdditionalCTA": () => (/* binding */ AdditionalCTA) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +const AdditionalCTA = ({ + content, + handleAction +}) => { + var _content$additional_b, _content$additional_b4, _content$additional_b5; + + let buttonStyle = ""; + + if (!((_content$additional_b = content.additional_button) !== null && _content$additional_b !== void 0 && _content$additional_b.style)) { + buttonStyle = "primary"; + } else { + var _content$additional_b2, _content$additional_b3; + + buttonStyle = ((_content$additional_b2 = content.additional_button) === null || _content$additional_b2 === void 0 ? void 0 : _content$additional_b2.style) === "link" ? "cta-link" : (_content$additional_b3 = content.additional_button) === null || _content$additional_b3 === void 0 ? void 0 : _content$additional_b3.style; + } + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { + text: (_content$additional_b4 = content.additional_button) === null || _content$additional_b4 === void 0 ? void 0 : _content$additional_b4.label + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { + className: `${buttonStyle} additional-cta`, + onClick: handleAction, + value: "additional_button", + disabled: ((_content$additional_b5 = content.additional_button) === null || _content$additional_b5 === void 0 ? void 0 : _content$additional_b5.disabled) === true + })); +}; + +/***/ }), +/* 16 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "EmbeddedMigrationWizard": () => (/* binding */ EmbeddedMigrationWizard) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EmbeddedMigrationWizard = ({ + handleAction +}) => { + const ref = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { + const handleBeginMigration = () => { + handleAction({ + currentTarget: { + value: "migrate_start" + }, + source: "primary_button" + }); + }; + + const handleClose = () => { + handleAction({ + currentTarget: { + value: "migrate_close" + } + }); + }; + + const { + current + } = ref; + current === null || current === void 0 ? void 0 : current.addEventListener("MigrationWizard:BeginMigration", handleBeginMigration); + current === null || current === void 0 ? void 0 : current.addEventListener("MigrationWizard:Close", handleClose); + return () => { + current === null || current === void 0 ? void 0 : current.removeEventListener("MigrationWizard:BeginMigration", handleBeginMigration); + current === null || current === void 0 ? void 0 : current.removeEventListener("MigrationWizard:Close", handleClose); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("migration-wizard", { + "auto-request-state": "", + ref: ref + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("panel-list", null)); +}; + +/***/ }), +/* 17 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "BASE_PARAMS": () => (/* binding */ BASE_PARAMS), +/* harmony export */ "addUtmParams": () => (/* binding */ addUtmParams) +/* harmony export */ }); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * BASE_PARAMS keys/values can be modified from outside this file + */ +const BASE_PARAMS = { + utm_source: "activity-stream", + utm_campaign: "firstrun", + utm_medium: "referral" +}; +/** + * Takes in a url as a string or URL object and returns a URL object with the + * utm_* parameters added to it. If a URL object is passed in, the paraemeters + * are added to it (the return value can be ignored in that case as it's the + * same object). + */ + +function addUtmParams(url, utmTerm) { + let returnUrl = url; + + if (typeof returnUrl === "string") { + returnUrl = new URL(url); + } + + for (let [key, value] of Object.entries(BASE_PARAMS)) { + if (!returnUrl.searchParams.has(key)) { + returnUrl.searchParams.append(key, value); + } + } + + returnUrl.searchParams.append("utm_term", utmTerm); + return returnUrl; +} + +/***/ }), +/* 18 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "ReturnToAMO": () => (/* binding */ ReturnToAMO) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3); +/* harmony import */ var _MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6); +/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(17); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + + +class ReturnToAMO extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) { + constructor(props) { + super(props); + this.fetchFlowParams = this.fetchFlowParams.bind(this); + this.handleAction = this.handleAction.bind(this); + } + + async fetchFlowParams() { + if (this.props.metricsFlowUri) { + this.setState({ + flowParams: await _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.fetchFlowParams(this.props.metricsFlowUri) + }); + } + } + + componentDidUpdate() { + this.fetchFlowParams(); + } + + handleAction(event) { + const { + content, + message_id, + url, + utm_term + } = this.props; + let { + action, + source_id + } = content[event.currentTarget.value]; + let { + type, + data + } = action; + + if (type === "INSTALL_ADDON_FROM_URL") { + if (!data) { + return; + } // Set add-on url in action.data.url property from JSON + + + data = { ...data, + url + }; + } else if (type === "SHOW_FIREFOX_ACCOUNTS") { + let params = { ..._asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_3__.BASE_PARAMS, + utm_term: `aboutwelcome-${utm_term}-screen` + }; + + if (action.addFlowParams && this.state.flowParams) { + params = { ...params, + ...this.state.flowParams + }; + } + + data = { ...data, + extraParams: params + }; + } + + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.handleUserAction({ + type, + data + }); + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.sendActionTelemetry(message_id, source_id); + } + + render() { + var _this$props$themeScre; + + const { + content, + type + } = this.props; + + if (!content) { + return null; + } + + if (content !== null && content !== void 0 && content.primary_button.label) { + content.primary_button.label.string_id = type.includes("theme") ? "return-to-amo-add-theme-label" : "mr1-return-to-amo-add-extension-label"; + } // For experiments, when needed below rendered UI allows settings hard coded strings + // directly inside JSON except for ReturnToAMOText which picks add-on name and icon from fluent string + + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "outer-wrapper onboardingContainer proton", + style: content.backdrop ? { + background: content.backdrop + } : {} + }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_2__.MultiStageProtonScreen, { + content: content, + isRtamo: true, + isTheme: type.includes("theme"), + id: this.props.message_id, + order: this.props.order || 0, + totalNumberOfScreens: 1, + isSingleScreen: true, + autoAdvance: this.props.auto_advance, + iconURL: type.includes("theme") ? (_this$props$themeScre = this.props.themeScreenshots[0]) === null || _this$props$themeScre === void 0 ? void 0 : _this$props$themeScre.url : this.props.iconURL, + addonName: this.props.name, + handleAction: this.handleAction + })); + } + +} +ReturnToAMO.defaultProps = _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__.DEFAULT_RTAMO_CONTENT; + +/***/ }) +/******/ ]); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __webpack_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +(() => { +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); +/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); +/* harmony import */ var _components_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); +/* harmony import */ var _components_ReturnToAMO__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(18); +function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + + + + +class AboutWelcome extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) { + constructor(props) { + super(props); + this.state = { + metricsFlowUri: null + }; + this.fetchFxAFlowUri = this.fetchFxAFlowUri.bind(this); + } + + async fetchFxAFlowUri() { + var _window$AWGetFxAMetri, _window; + + this.setState({ + metricsFlowUri: await ((_window$AWGetFxAMetri = (_window = window).AWGetFxAMetricsFlowURI) === null || _window$AWGetFxAMetri === void 0 ? void 0 : _window$AWGetFxAMetri.call(_window)) + }); + } + + componentDidMount() { + if (!this.props.skipFxA) { + this.fetchFxAFlowUri(); + } // Record impression with performance data after allowing the page to load + + + const recordImpression = domState => { + const { + domComplete, + domInteractive + } = performance.getEntriesByType("navigation").pop(); + _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendImpressionTelemetry(this.props.messageId, { + domComplete, + domInteractive, + mountStart: performance.getEntriesByName("mount").pop().startTime, + domState, + source: this.props.UTMTerm + }); + }; + + if (document.readyState === "complete") { + // Page might have already triggered a load event because it waited for async data, + // e.g., attribution, so the dom load timing could be of a empty content + // with domState in telemetry captured as 'complete' + recordImpression(document.readyState); + } else { + window.addEventListener("load", () => recordImpression("load"), { + once: true + }); + } // Captures user has seen about:welcome by setting + // firstrun.didSeeAboutWelcome pref to true and capturing welcome UI unique messageId + + + window.AWSendToParent("SET_WELCOME_MESSAGE_SEEN", this.props.messageId); + } + + render() { + const { + props + } = this; + + if (props.template === "return_to_amo") { + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_ReturnToAMO__WEBPACK_IMPORTED_MODULE_4__.ReturnToAMO, { + message_id: props.messageId, + type: props.type, + name: props.name, + url: props.url, + iconURL: props.iconURL, + themeScreenshots: props.screenshots, + metricsFlowUri: this.state.metricsFlowUri + }); + } + + return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_3__.MultiStageAboutWelcome, { + message_id: props.messageId, + defaultScreens: props.screens, + updateHistory: !props.disableHistoryUpdates, + metricsFlowUri: this.state.metricsFlowUri, + utm_term: props.UTMTerm, + transitions: props.transitions, + backdrop: props.backdrop, + startScreen: props.startScreen || 0, + appAndSystemLocaleInfo: props.appAndSystemLocaleInfo + }); + } + +} // Computes messageId and UTMTerm info used in telemetry + + +function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) { + let messageId = welcomeContent.template === "return_to_amo" ? `RTAMO_DEFAULT_WELCOME_${welcomeContent.type.toUpperCase()}` : "DEFAULT_ID"; + let UTMTerm = "aboutwelcome-default"; + + if (welcomeContent.id) { + messageId = welcomeContent.id.toUpperCase(); + } + + if (experimentId && branchId) { + UTMTerm = `aboutwelcome-${experimentId}-${branchId}`.toLowerCase(); + } + + return { + messageId, + UTMTerm + }; +} + +async function retrieveRenderContent() { + // Feature config includes RTAMO attribution data if exists + // else below data in order specified + // user prefs + // experiment data + // defaults + let featureConfig = await window.AWGetFeatureConfig(); + let { + messageId, + UTMTerm + } = ComputeTelemetryInfo(featureConfig, featureConfig.slug, featureConfig.branch && featureConfig.branch.slug); + return { + featureConfig, + messageId, + UTMTerm + }; +} + +async function mount() { + let { + featureConfig: aboutWelcomeProps, + messageId, + UTMTerm + } = await retrieveRenderContent(); + react_dom__WEBPACK_IMPORTED_MODULE_1___default().render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(AboutWelcome, _extends({ + messageId: messageId, + UTMTerm: UTMTerm + }, aboutWelcomeProps)), document.getElementById("multi-stage-message-root")); +} + +performance.mark("mount"); +mount(); +})(); + +/******/ })() +; \ No newline at end of file diff --git a/browser/components/newtab/aboutwelcome/content/aboutwelcome.css b/browser/components/newtab/aboutwelcome/content/aboutwelcome.css new file mode 100644 index 0000000000..ba9c835693 --- /dev/null +++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.css @@ -0,0 +1,1644 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.onboardingContainer.featureCallout { + --fc-background: var(--fc-background-light, #FFF); + --fc-color: var(--fc-color-light, #15141A); + --fc-border: var(--fc-border-light, #CFCFD8); + --fc-accent-color: var(--fc-accent-color-light, #0061E0); + --fc-button-background: var(--fc-button-background-light, #F0F0F4); + --fc-button-color: var(--fc-button-color-light, #15141A); + --fc-button-border: var(--fc-button-border-light, transparent); + --fc-button-background-hover: var(--fc-button-background-hover-light, #E0E0E6); + --fc-button-color-hover: var(--fc-button-color-hover-light, #15141A); + --fc-button-border-hover: var(--fc-button-border-hover-light, transparent); + --fc-button-background-active: var(--fc-button-background-active-light, #CFCFD8); + --fc-button-color-active: var(--fc-button-color-active-light, #15141A); + --fc-button-border-active: var(--fc-button-border-active-light, transparent); + --fc-step-color: color-mix(in srgb, currentColor 50%, transparent); + position: absolute; + transition: opacity 0.5s ease; + z-index: 2147483645; + outline: none; + color: var(--fc-color); +} +@media (prefers-color-scheme: dark) { + .onboardingContainer.featureCallout { + --fc-background: var(--fc-background-dark, #42414D); + --fc-color: var(--fc-color-dark, #FBFBFE); + --fc-border: var(--fc-border-dark, #3A3944); + --fc-accent-color: var(--fc-accent-color-dark, #00DDFF); + --fc-button-background: var(--fc-button-background-dark, #2B2A33); + --fc-button-color: var(--fc-button-color-dark, #FBFBFE); + --fc-button-border: var(--fc-button-border-dark, transparent); + --fc-button-background-hover: var(--fc-button-background-hover-dark, #52525E); + --fc-button-color-hover: var(--fc-button-color-hover-dark, #FBFBFE); + --fc-button-border-hover: var(--fc-button-border-hover-dark, transparent); + --fc-button-background-active: var(--fc-button-background-active-dark, #5B5B66); + --fc-button-color-active: var(--fc-button-color-active-dark, #FBFBFE); + --fc-button-border-active: var(--fc-button-border-active-dark, transparent); + } +} +@media (prefers-contrast) { + .onboardingContainer.featureCallout { + --fc-background: var(--fc-background-hcm, -moz-dialog); + --fc-color: var(--fc-color-hcm, -moz-dialogtext); + --fc-border: var(--fc-border-hcm, -moz-dialogtext); + --fc-accent-color: var(--fc-accent-color-hcm, LinkText); + --fc-button-background: var(--fc-button-background-hcm, ButtonFace); + --fc-button-color: var(--fc-button-color-hcm, ButtonText); + --fc-button-border: var(--fc-button-border-hcm, ButtonText); + --fc-button-background-hover: var(--fc-button-background-hover-hcm, ButtonText); + --fc-button-color-hover: var(--fc-button-color-hover-hcm, ButtonFace); + --fc-button-border-hover: var(--fc-button-border-hover-hcm, ButtonText); + --fc-button-background-active: var(--fc-button-background-active-hcm, ButtonText); + --fc-button-color-active: var(--fc-button-color-active-hcm, ButtonFace); + --fc-button-border-active: var(--fc-button-border-active-hcm, ButtonText); + --fc-step-color: var(--fc-accent-color-hcm, LinkText); + } +} +@media (-moz-content-prefers-color-scheme: light) { + .onboardingContainer.featureCallout.simulateContent { + --fc-background: var(--fc-background-light, #FFF); + --fc-color: var(--fc-color-light, #15141A); + --fc-border: var(--fc-border-light, #CFCFD8); + --fc-accent-color: var(--fc-accent-color-light, #0061E0); + --fc-button-background: var(--fc-button-background-light, #F0F0F4); + --fc-button-color: var(--fc-button-color-light, #15141A); + --fc-button-border: var(--fc-button-border-light, transparent); + --fc-button-background-hover: var(--fc-button-background-hover-light, #E0E0E6); + --fc-button-color-hover: var(--fc-button-color-hover-light, #15141A); + --fc-button-border-hover: var(--fc-button-border-hover-light, transparent); + --fc-button-background-active: var(--fc-button-background-active-light, #CFCFD8); + --fc-button-color-active: var(--fc-button-color-active-light, #15141A); + --fc-button-border-active: var(--fc-button-border-active-light, transparent); + --fc-step-color: color-mix(in srgb, currentColor 50%, transparent); + } +} +@media (-moz-content-prefers-color-scheme: dark) { + .onboardingContainer.featureCallout.simulateContent { + --fc-background: var(--fc-background-dark, #42414D); + --fc-color: var(--fc-color-dark, #FBFBFE); + --fc-border: var(--fc-border-dark, #3A3944); + --fc-accent-color: var(--fc-accent-color-dark, #00DDFF); + --fc-button-background: var(--fc-button-background-dark, #2B2A33); + --fc-button-color: var(--fc-button-color-dark, #FBFBFE); + --fc-button-border: var(--fc-button-border-dark, transparent); + --fc-button-background-hover: var(--fc-button-background-hover-dark, #52525E); + --fc-button-color-hover: var(--fc-button-color-hover-dark, #FBFBFE); + --fc-button-border-hover: var(--fc-button-border-hover-dark, transparent); + --fc-button-background-active: var(--fc-button-background-active-dark, #5B5B66); + --fc-button-color-active: var(--fc-button-color-active-dark, #FBFBFE); + --fc-button-border-active: var(--fc-button-border-active-dark, transparent); + } +} +@media (prefers-contrast) { + .onboardingContainer.featureCallout.simulateContent { + --fc-background: var(--fc-background-hcm, -moz-dialog); + --fc-color: var(--fc-color-hcm, -moz-dialogtext); + --fc-border: var(--fc-border-hcm, -moz-dialogtext); + --fc-accent-color: var(--fc-accent-color-hcm, LinkText); + --fc-button-background: var(--fc-button-background-hcm, ButtonFace); + --fc-button-color: var(--fc-button-color-hcm, ButtonText); + --fc-button-border: var(--fc-button-border-hcm, ButtonText); + --fc-button-background-hover: var(--fc-button-background-hover-hcm, ButtonText); + --fc-button-color-hover: var(--fc-button-color-hover-hcm, ButtonFace); + --fc-button-border-hover: var(--fc-button-border-hover-hcm, ButtonText); + --fc-button-background-active: var(--fc-button-background-active-hcm, ButtonText); + --fc-button-color-active: var(--fc-button-color-active-hcm, ButtonFace); + --fc-button-border-active: var(--fc-button-border-active-hcm, ButtonText); + --fc-step-color: var(--fc-accent-color-hcm, LinkText); + } +} +.onboardingContainer.featureCallout.hidden { + opacity: 0; + pointer-events: none; +} +.onboardingContainer.featureCallout, .onboardingContainer.featureCallout .outer-wrapper { + height: auto; +} +.onboardingContainer.featureCallout .screen:dir(rtl) { + transform: none; +} +.onboardingContainer.featureCallout .screen[pos=callout] { + height: fit-content; + min-height: unset; + overflow: visible; +} +.onboardingContainer.featureCallout .screen[pos=callout] .logo-container { + display: flex; + justify-content: center; +} +.onboardingContainer.featureCallout .screen[pos=callout] .logo-container .brand-logo { + margin: 30px 45px 0; +} +.onboardingContainer.featureCallout .screen[pos=callout] .logo-container .brand-logo:dir(rtl) { + transform: rotateY(180deg); +} +.onboardingContainer.featureCallout .screen[pos=callout] .welcome-text { + align-items: baseline; + text-align: start; + margin-inline: 30px; + padding: 20px 0 0; +} +.onboardingContainer.featureCallout .screen[pos=callout] .welcome-text h1, +.onboardingContainer.featureCallout .screen[pos=callout] .welcome-text h2 { + font-size: 0.8em; + margin: 0; + color: inherit; +} +.onboardingContainer.featureCallout .screen[pos=callout] .welcome-text h1 { + font-weight: bold; +} +.onboardingContainer.featureCallout .screen[pos=callout] .welcome-text h2 { + margin-block: 10px; +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-secondary { + display: none; +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main { + height: fit-content; + width: fit-content; +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main .main-content { + position: relative; + overflow: hidden; + border: 1px solid var(--fc-border); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + border-radius: 4px; + padding-top: 0; + padding-bottom: 26px; + width: 25em; + background-color: var(--fc-background); +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main .main-content .steps { + height: auto; + position: absolute; + bottom: 42px; + margin: 0 30px; + padding-block: 0; +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main .main-content .steps .indicator { + border: 4px solid var(--fc-step-color); +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main .main-content .steps .indicator.current { + border-color: var(--fc-accent-color); +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main .main-content .steps .indicator.current, .onboardingContainer.featureCallout .screen[pos=callout] .section-main .main-content .steps.progress-bar .indicator.complete { + border-color: var(--fc-accent-color); +} +.onboardingContainer.featureCallout .screen[pos=callout] .section-main .dismiss-button { + font-size: 1em; + top: 0; + margin-block: 15px 0; + margin-inline: 0 15px; + z-index: 2147483647; + background-color: var(--fc-background); +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .primary, +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .secondary-cta .secondary { + padding: 4px 16px; + font-size: 0.8em; + height: 2em; + background-color: var(--fc-button-background); +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .primary { + font-weight: bold; + float: inline-end; + margin-inline: 10px 30px; + padding: 4px 16px; + font-size: 0.8em; + line-height: 16px; + min-height: 32px; +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .secondary-cta { + float: inline-end; +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .primary, +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .secondary-cta .secondary, +.onboardingContainer.featureCallout .screen[pos=callout] .dismiss-button { + border-radius: 4px; + border: 1px solid var(--fc-button-border); + cursor: pointer; + color: var(--fc-button-color); +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .primary:hover, +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .secondary-cta .secondary:hover, +.onboardingContainer.featureCallout .screen[pos=callout] .dismiss-button:hover { + background-color: var(--fc-button-background-hover); + color: var(--fc-button-color-hover); + border: 1px solid var(--fc-button-border-hover); +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .primary:hover:active, +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .secondary-cta .secondary:hover:active, +.onboardingContainer.featureCallout .screen[pos=callout] .dismiss-button:hover:active { + background-color: var(--fc-button-background-active); + color: var(--fc-button-color-active); + border: 1px solid var(--fc-button-border-active); +} +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .primary:focus-visible, +.onboardingContainer.featureCallout .screen[pos=callout] .action-buttons .secondary-cta .secondary:focus-visible, +.onboardingContainer.featureCallout .screen[pos=callout] .dismiss-button:focus-visible { + box-shadow: none; + outline: 2px solid var(--fc-accent-color); + outline-offset: 2px; +} +.onboardingContainer.featureCallout.callout-arrow::before, .onboardingContainer.featureCallout.callout-arrow::after { + content: ""; + position: absolute; + width: 24px; + height: 24px; + transform: rotate(45deg); + transform-style: preserve-3d; +} +.onboardingContainer.featureCallout.callout-arrow::before { + z-index: 2147483647; + background-color: var(--fc-background); +} +.onboardingContainer.featureCallout.callout-arrow::after { + background: transparent; + outline: 1px solid var(--fc-border); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + z-index: -1; +} +.onboardingContainer.featureCallout.arrow-top::before, .onboardingContainer.featureCallout.arrow-top::after { + top: -11px; + inset-inline-start: calc(50% - 12px); +} +.onboardingContainer.featureCallout.arrow-bottom::before, .onboardingContainer.featureCallout.arrow-bottom::after { + bottom: -11px; + inset-inline-start: calc(50% - 12px); +} +.onboardingContainer.featureCallout.arrow-inline-end::before, .onboardingContainer.featureCallout.arrow-inline-end::after { + top: calc(50% - 12px); + inset-inline-end: -11px; +} +.onboardingContainer.featureCallout.arrow-inline-start::before, .onboardingContainer.featureCallout.arrow-inline-start::after { + top: calc(50% - 12px); + inset-inline-start: -11px; +} +.onboardingContainer.featureCallout.arrow-top-end::before, .onboardingContainer.featureCallout.arrow-top-end::after { + top: -11px; + inset-inline-end: 12px; +} +.onboardingContainer.featureCallout.arrow-top-start::before, .onboardingContainer.featureCallout.arrow-top-start::after { + top: -11px; + inset-inline-start: 12px; +} +.onboardingContainer.featureCallout.hidden-arrow::before, .onboardingContainer.featureCallout.hidden-arrow::after { + display: none; +} +.onboardingContainer.featureCallout:focus-visible .screen[pos=callout] .section-main .main-content { + outline: 2px solid var(--fc-accent-color); + border-color: transparent; +} +@media (prefers-contrast) { + .onboardingContainer.featureCallout:focus-visible .screen[pos=callout] .section-main .main-content { + border-color: var(--fc-background); + } +} +.onboardingContainer.featureCallout:focus-visible.callout-arrow::after { + outline: 2px solid var(--fc-accent-color); +} + +html { + height: 100%; +} + +.dummy { + background: var(--mr-welcome-background-color) var(--mr-welcome-background-gradient) var(--mr-secondary-position) var(--mr-screen-background-color); +} + +:root[dialogroot] { + background-color: transparent; +} +:root[dialogroot] body { + padding: 0; +} +:root[dialogroot] .onboardingContainer { + height: 100%; + background-color: transparent; +} +:root[dialogroot] .onboardingContainer:dir(rtl) { + transform: unset; +} +:root[dialogroot] .onboardingContainer .logo-container { + pointer-events: none; +} +:root[dialogroot] .onboardingContainer .screen:dir(rtl) { + transform: unset; +} + +.welcome-container .onboardingContainer { + min-height: 610px; + min-width: fit-content; +} + +.onboardingContainer { + --grey-subtitle-1: #696977; + --mr-welcome-background-color: #F8F6F4; + --mr-screen-heading-color: var(--in-content-text-color); + --mr-welcome-background-gradient: linear-gradient(0deg, rgba(144, 89, 255, 20%) 0%, rgba(2, 144, 238, 20%) 100%); + --mr-screen-background-color: #F8F6F4; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; + font-size: 16px; + position: relative; + text-align: center; + height: 100vh; + --transition: 0.6s opacity, 0.6s scale, 0.6s rotate, 0.6s translate; +} +@media (prefers-color-scheme: dark) { + .onboardingContainer { + --grey-subtitle-1: #FFF; + --mr-welcome-background-color: #333336; + --mr-welcome-background-gradient: linear-gradient(0deg, rgba(144, 89, 255, 30%) 0%, rgba(2, 144, 238, 30%) 100%); + --mr-screen-background-color: #62697A; + } +} +@media (prefers-contrast) { + .onboardingContainer { + --mr-screen-background-color: buttontext; + --mr-screen-heading-color: buttonface; + background-color: var(--in-content-page-background); + } +} +@media (prefers-reduced-motion: no-preference) { + .onboardingContainer { + --translate: 30px; + --rotate: 20deg; + --scale: 0.4; + --progress-bar-transition: 0.6s translate; + } + .onboardingContainer:dir(rtl) { + --scale: -0.4 0.4; + } +} +@media (prefers-reduced-motion: reduce) { + .onboardingContainer { + --translate: 0; + --rotate: 0deg; + --scale: 1; + --progress-bar-transition: none; + } + .onboardingContainer:dir(rtl) { + --scale: -1 1; + } +} +.onboardingContainer:dir(rtl) { + transform: rotateY(180deg); +} +.onboardingContainer .section-main { + display: flex; + flex-direction: column; + justify-content: center; + width: 504px; + flex-shrink: 0; +} +.onboardingContainer .section-main:not(.embedded-migration) { + position: relative; +} +.onboardingContainer .main-content { + background-color: var(--in-content-page-background); + border-radius: 20px; + box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + transition: var(--transition); + z-index: 1; + box-sizing: border-box; +} +.onboardingContainer .main-content.no-steps { + padding-bottom: 48px; +} +.onboardingContainer .main-content .main-content-inner { + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: space-around; +} +.onboardingContainer .main-content .no-steps .main-content { + padding-bottom: 48px; +} +.onboardingContainer .main-content .no-steps .steps { + display: none; +} +.onboardingContainer .screen { + display: flex; + position: relative; + flex-flow: row nowrap; + height: 100%; + min-height: 500px; + overflow: hidden; + --in-content-link-color: var(--in-content-primary-button-background); + --in-content-link-color-hover: var(--in-content-primary-button-background-hover); + --in-content-link-color-active: var(--in-content-primary-button-background-active); + --in-content-link-color-visited: var(--in-content-link-color); +} +.onboardingContainer .screen.light-text { + --in-content-page-color: rgb(251, 251, 254); + --in-content-primary-button-text-color: rgb(43, 42, 51); + --in-content-primary-button-text-color-hover: rgb(43, 42, 51); + --in-content-primary-button-background: rgb(0, 221, 255); + --in-content-primary-button-background-hover: rgb(128, 235, 255); + --in-content-primary-button-background-active: rgb(170, 242, 255); + --checkbox-checked-bgcolor: var(--in-content-primary-button-background); + --in-content-button-text-color: var(--in-content-page-color); +} +.onboardingContainer .screen.dark-text { + --in-content-page-color: rgb(21, 20, 26); + --in-content-primary-button-text-color: rgb(251, 251, 254); + --in-content-primary-button-text-color-hover: rgb(251, 251, 254); + --in-content-primary-button-background: #0061E0; + --in-content-primary-button-background-hover: #0250BB; + --in-content-primary-button-background-active: #053E94; + --in-content-primary-button-border-color: transparent; + --in-content-primary-button-border-hover: transparent; + --checkbox-checked-bgcolor: var(--in-content-primary-button-background); + --in-content-button-text-color: var(--in-content-page-color); +} +.onboardingContainer .screen:dir(rtl) { + transform: rotateY(180deg); +} +.onboardingContainer .screen[pos=center] { + background-color: rgba(21, 20, 26, 0.5); + min-width: 504px; +} +.onboardingContainer .screen[pos=center].with-noodles { + min-width: 610px; + min-height: 610px; +} +.onboardingContainer .screen[pos=center].with-noodles .section-main { + height: 504px; +} +.onboardingContainer .screen[pos=center].with-video { + justify-content: center; + background: none; + align-items: center; +} +.onboardingContainer .screen[pos=center].with-video .section-main { + width: 800px; + height: 550px; +} +.onboardingContainer .screen[pos=center].with-video .main-content { + background-color: var(--mr-welcome-background-color); + border-radius: 8px; + box-shadow: 0 2px 14px rgba(58, 57, 68, 0.2); + padding: 44px 85px 20px; +} +.onboardingContainer .screen[pos=center].with-video .main-content .welcome-text { + margin: 0; +} +.onboardingContainer .screen[pos=center].with-video .main-content .main-content-inner { + justify-content: space-between; +} +.onboardingContainer .screen[pos=center].with-video .main-content h1, +.onboardingContainer .screen[pos=center].with-video .main-content h2 { + align-self: start; +} +.onboardingContainer .screen[pos=center].with-video .main-content h1 { + font-size: 24px; + line-height: 28.8px; +} +.onboardingContainer .screen[pos=center].with-video .main-content h2 { + font-size: 15px; + line-height: 22px; +} +.onboardingContainer .screen[pos=center].with-video .main-content .secondary-cta { + justify-content: end; +} +.onboardingContainer .screen[pos=center].with-video .main-content .secondary-cta .arrow-icon { + -moz-context-properties: fill; + fill: currentColor; + text-decoration: none; +} +.onboardingContainer .screen[pos=center].with-video .main-content .secondary-cta .arrow-icon::after { + content: ""; + padding-inline-end: 12px; + margin-inline-start: 4px; + background: url("chrome://browser/skin/forward.svg") no-repeat center/12px; +} +.onboardingContainer .screen[pos=center].with-video .main-content .secondary-cta .arrow-icon:dir(rtl)::after { + background-image: url("chrome://browser/skin/back.svg"); +} +.onboardingContainer .screen[pos=center].with-video .main-content .secondary-cta .secondary { + background-color: var(--in-content-button-background) !important; + border: 1px solid var(--in-content-button-border-color); + line-height: 12px; + font-size: 0.72em; + font-weight: 600; + padding: 8px 16px; + text-decoration: none; + color: var(--in-content-button-text-color); +} +.onboardingContainer .screen[pos=center].with-video .main-content .secondary-cta .secondary:hover { + background-color: var(--in-content-button-background-hover) !important; + color: var(--in-content-button-text-color-hover); +} +.onboardingContainer .screen:not([pos=split]) .secondary-cta .secondary { + background: none; + color: var(--in-content-link-color); + font-size: 14px; + font-weight: normal; + line-height: 20px; +} +.onboardingContainer .screen:not([pos=split]) .secondary-cta.top button { + color: #FFF; +} +.onboardingContainer .screen:not([pos=split]) .secondary-cta.top button:hover { + color: #E0E0E6; +} +.onboardingContainer .screen[pos=split] { + margin: auto; + min-height: 550px; +} +.onboardingContainer .screen[pos=split]::before { + content: ""; + position: absolute; + box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2); + width: 800px; + height: 550px; + border-radius: 8px; + inset: 0; + margin: auto; + pointer-events: none; +} +.onboardingContainer .screen[pos=split] .section-secondary, +.onboardingContainer .screen[pos=split] .section-main { + width: 400px; + height: 550px; +} +.onboardingContainer .screen[pos=split] .secondary-cta.top { + position: fixed; + padding-inline-end: 0; +} +.onboardingContainer .screen[pos=split] .secondary-cta.top button { + color: var(--in-content-page-color); +} +.onboardingContainer .screen[pos=split] .section-main { + flex-direction: row; + display: block; + margin: auto auto auto 0; +} +.onboardingContainer .screen[pos=split] .section-main:dir(rtl) { + margin: auto 0 auto auto; +} +.onboardingContainer .screen[pos=split] .section-main .main-content { + border-radius: 0 8px 8px 0; + overflow: hidden; + padding-inline: 35px 20px; + padding-block: 120px 0; + box-shadow: none; +} +.onboardingContainer .screen[pos=split] .section-main .main-content.no-steps { + padding-bottom: 48px; +} +.onboardingContainer .screen[pos=split] .section-main .main-content:dir(rtl) { + border-radius: 8px 0 0 8px; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .main-content-inner { + min-height: 330px; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .main-content-inner .language-switcher-container .primary { + margin-bottom: 5px; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons { + position: relative; + text-align: initial; + height: 100%; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .checkbox-container { + font-size: 13px; + margin-block: 1em; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .checkbox-container:not(.multi-select-item) { + transition: var(--transition); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .checkbox-container input, +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .checkbox-container label { + vertical-align: middle; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta { + margin: 8px 0; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta.cta-link { + background: none; + padding: 0; + font-weight: normal; + text-decoration: underline; + cursor: pointer; + color: var(--in-content-link-color); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta.cta-link:hover { + text-decoration: none; + color: var(--in-content-link-color-hover); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta.cta-link:active { + text-decoration: none; + color: var(--in-content-link-color-active); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta.secondary:hover { + background-color: var(--in-content-button-background-hover); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons.additional-cta-container { + flex-wrap: nowrap; + align-items: start; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta { + position: absolute; + bottom: -30px; + inset-inline-end: 0; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .secondary { + background-color: var(--in-content-button-background) !important; + border: 1px solid var(--in-content-button-border-color); + line-height: 12px; + font-size: 0.72em; + font-weight: 600; + padding: 8px 16px; + text-decoration: none; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .secondary:hover { + background-color: var(--in-content-button-background-hover) !important; + color: var(--in-content-button-text-color-hover); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .arrow-icon { + -moz-context-properties: fill; + fill: currentColor; + text-decoration: none; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .arrow-icon::after { + content: ""; + padding-inline-end: 12px; + margin-inline-start: 4px; + background: url("chrome://browser/skin/forward.svg") no-repeat center/12px; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .arrow-icon:dir(rtl)::after { + background-image: url("chrome://browser/skin/back.svg"); +} +.onboardingContainer .screen[pos=split] .section-main .main-content .logo-container { + text-align: start; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .brand-logo { + height: 25px; + margin-block: 0; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text { + margin-inline: 0 10px; + margin-block: 10px 35px; + text-align: initial; + align-items: initial; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text:empty { + margin: 0; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text h1 { + font-size: 24px; + line-height: 1.2; + width: 300px; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text h2 { + margin: 10px 0 0; + min-height: 1em; + font-size: 15px; + line-height: 1.5; +} +@media (prefers-contrast: no-preference) { + .onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text h2 { + color: #5B5B66; + } +} +.onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text h1, +.onboardingContainer .screen[pos=split] .section-main .main-content .primary { + margin: 0; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .steps { + z-index: 1; +} +.onboardingContainer .screen[pos=split] .section-main .main-content .steps.progress-bar { + width: 400px; + margin-inline: -35px; +} +@media (prefers-contrast) { + .onboardingContainer .screen[pos=split] .section-main .main-content { + border: 1px solid var(--in-content-page-color); + } + .onboardingContainer .screen[pos=split] .section-main .main-content .steps.progress-bar { + border-top: 1px solid var(--in-content-page-color); + background-color: var(--in-content-page-background); + } + .onboardingContainer .screen[pos=split] .section-main .main-content .steps.progress-bar .indicator { + background-color: var(--in-content-accent-color); + } +} +.onboardingContainer .screen[pos=split] .section-secondary { + --mr-secondary-position: center center / auto 350px; + border-radius: 8px 0 0 8px; + margin: auto 0 auto auto; + display: flex; + align-items: center; + -moz-context-properties: fill, stroke, fill-opacity, stroke-opacity; + stroke: currentColor; +} +.onboardingContainer .screen[pos=split] .section-secondary:dir(rtl) { + border-radius: 0 8px 8px 0; + margin: auto auto auto 0; +} +.onboardingContainer .screen[pos=split] .section-secondary h1 { + color: var(--mr-screen-heading-color); + font-weight: 700; + font-size: 47px; + line-height: 110%; + max-width: 340px; + text-align: initial; + white-space: pre-wrap; + text-shadow: none; + margin-inline: 40px 0; +} +.onboardingContainer .screen[pos=split] .section-secondary .image-alt { + width: inherit; + height: inherit; +} +.onboardingContainer .screen[pos=split] .section-secondary .hero-image { + flex: 1; + display: flex; + justify-content: center; + max-height: 100%; +} +.onboardingContainer .screen[pos=split] .section-secondary .hero-image img { + width: 100%; + max-width: 180px; + margin: 25px 0; + padding-bottom: 30px; +} +@media only screen and (max-width: 800px) { + .onboardingContainer .screen[pos=split] .section-secondary .hero-image img { + padding-bottom: unset; + } +} +.onboardingContainer .screen[pos=split] .tiles-theme-container { + margin-block: -20px auto; + align-items: initial; +} +.onboardingContainer .screen[pos=split] .tiles-theme-container .colorway-text { + text-align: initial; + transition: var(--transition); + font-size: 13px; + line-height: 1.5; + min-height: 4.5em; + margin-block: 10px 20px; +} +.onboardingContainer .screen[pos=split] .tiles-theme-container .theme { + min-width: 38px; +} +@media (prefers-contrast: no-preference) and (prefers-color-scheme: dark) { + .onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text h2 { + color: #CFCFD8; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary { + background-color: #2B2A33; + } +} +@media only screen and (min-width: 800px) { + .onboardingContainer .screen[pos=split] .tiles-theme-section { + margin-inline-start: -10px; + } +} +@media only screen and (max-width: 800px) { + .onboardingContainer .screen[pos=split] { + flex-direction: column; + min-height: 550px; + } + .onboardingContainer .screen[pos=split]::before { + width: 400px; + } + .onboardingContainer .screen[pos=split] .section-secondary, +.onboardingContainer .screen[pos=split] .section-main { + width: 400px; + } + .onboardingContainer .screen[pos=split] .section-secondary { + --mr-secondary-background-position-y: top; + --mr-secondary-position: center var(--mr-secondary-background-position-y) / 75%; + border-radius: 8px 8px 0 0; + margin: auto auto 0; + height: 100px; + } + .onboardingContainer .screen[pos=split] .section-secondary .hero-image img { + margin: 6px 0; + } + .onboardingContainer .screen[pos=split] .section-secondary .message-text { + margin-inline: auto; + } + .onboardingContainer .screen[pos=split] .section-secondary h1 { + font-size: 35px; + text-align: center; + white-space: normal; + margin-inline: auto; + margin-block: 14px 6px; + } + .onboardingContainer .screen[pos=split] .section-secondary:dir(rtl) { + margin: auto auto 0; + border-radius: 8px 8px 0 0; + } + .onboardingContainer .screen[pos=split] .section-main { + margin: 0 auto auto; + height: 450px; + } + .onboardingContainer .screen[pos=split] .section-main .main-content { + border-radius: 0 0 8px 8px; + padding: 30px 0 0; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .main-content-inner { + align-items: center; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .logo-container { + text-align: center; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .logo-container .brand-logo { + min-height: 25px; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .logo-container .brand-logo, .onboardingContainer .screen[pos=split] .section-main .main-content .logo-container .brand-logo:dir(rtl) { + background-position: center; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text { + align-items: center; + text-align: center; + margin-inline: 0; + padding-inline: 30px; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text .spacer-bottom, +.onboardingContainer .screen[pos=split] .section-main .main-content .welcome-text .spacer-top { + display: none; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons { + text-align: center; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .checkbox-container { + display: none; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta { + position: relative; + margin-block: 10px 0; + bottom: 0; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .primary, +.onboardingContainer .screen[pos=split] .section-main .main-content .secondary { + min-width: 240px; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .colorway-text { + text-align: center; + margin-inline: 30px; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .steps { + padding-block: 0; + margin: 0; + } + .onboardingContainer .screen[pos=split] .section-main .main-content .steps.progress-bar { + margin-inline: 0; + } + .onboardingContainer .screen[pos=split] .section-main .additional-cta.cta-link { + align-self: center; + } + .onboardingContainer .screen[pos=split] .section-main .dismiss-button { + top: -100px; + } + .onboardingContainer .screen[pos=split] .section-main:dir(rtl) { + margin: 0 auto auto; + } + .onboardingContainer .screen[pos=split] .section-main:dir(rtl) .main-content { + border-radius: 0 0 8px 8px; + } +} +@media only screen and (max-height: 650px) and (min-width: 800px) and (max-width: 990px) { + .onboardingContainer .screen[pos=split] .section-main .secondary-cta.top { + display: none; + } +} +@media only screen and (max-height: 650px) and (max-width: 590px) { + .onboardingContainer .screen[pos=split] .section-main .secondary-cta.top { + position: absolute; + padding: 0; + top: 0; + inset-inline-end: 0; + } +} +.onboardingContainer .brand-logo { + margin-block: 60px 10px; + transition: var(--transition); + height: 80px; +} +.onboardingContainer .brand-logo.cta-top { + margin-top: 25px; +} +.onboardingContainer .brand-logo.hide { + visibility: hidden; + padding: unset; + margin-top: 50px; +} +.onboardingContainer .rtamo-theme-icon { + max-height: 30px; + border-radius: 2px; + margin-bottom: 10px; + margin-top: 24px; +} +.onboardingContainer .rtamo-icon { + text-align: start; +} +@media only screen and (max-width: 800px) { + .onboardingContainer .rtamo-icon { + text-align: center; + } +} +.onboardingContainer .text-link { + text-decoration: underline; + cursor: pointer; + color: var(--in-content-link-color); +} +.onboardingContainer .text-link:hover { + text-decoration: none; + color: var(--in-content-link-color-hover); +} +.onboardingContainer .text-link:active { + text-decoration: none; + color: var(--in-content-link-color-active); +} +.onboardingContainer .welcome-text { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 0.5em 1em; + transition: var(--transition); +} +.onboardingContainer .welcome-text h1, +.onboardingContainer .welcome-text h2 { + color: var(--in-content-page-color); + line-height: 1.5; +} +.onboardingContainer .welcome-text h1 { + font-size: 24px; + font-weight: 600; + margin: 0 6px; + letter-spacing: -0.02em; + outline: none; +} +.onboardingContainer .welcome-text h2 { + font-size: 16px; + font-weight: normal; + margin: 10px 6px 0; + max-width: 750px; + letter-spacing: -0.01em; +} +.onboardingContainer .welcome-text.fancy h1 { + background-image: linear-gradient(90deg, #9059FF, #FF4AA2, #FF8C00, #FF4AA2, #9059FF); + background-clip: text; + background-size: 200%; +} +@media (prefers-contrast: no-preference) { + .onboardingContainer .welcome-text.fancy h1 { + color: transparent; + } +} +@media (prefers-color-scheme: dark) { + .onboardingContainer .welcome-text.fancy h1 { + background-image: linear-gradient(90deg, #C688FF, #FF84C0, #FFBD4F, #FF84C0, #C688FF); + } + .onboardingContainer .welcome-text.fancy h1::selection { + color: #FFF; + background-color: #696977; + } +} +.onboardingContainer .welcome-text.shine h1 { + animation: shine 50s linear infinite; + background-size: 400%; +} +@keyframes shine { + to { + background-position: 400%; + } +} +.onboardingContainer .welcome-text .cta-paragraph a { + margin: 0; + text-decoration: underline; + cursor: pointer; +} +.onboardingContainer .screen.light-text .welcome-text.fancy h1 { + background-image: linear-gradient(90deg, #C688FF, #FF84C0, #FFBD4F, #FF84C0, #C688FF); +} +.onboardingContainer .screen.dark-text .welcome-text.fancy h1 { + background-image: linear-gradient(90deg, #9059FF, #FF4AA2, #FF8C00, #FF4AA2, #9059FF); +} +.onboardingContainer .welcomeZap span { + position: relative; + z-index: 1; + white-space: nowrap; +} +.onboardingContainer .welcomeZap .zap::after { + display: block; + background-repeat: no-repeat; + background-size: 100% 100%; + content: ""; + position: absolute; + top: calc(100% - 0.15em); + width: 100%; + height: 0.3em; + left: 0; + z-index: -1; + transform: scaleY(3); +} +.onboardingContainer .welcomeZap .zap.short::after { + background-image: url("chrome://activity-stream/content/data/content/assets/short-zap.svg"); +} +.onboardingContainer .welcomeZap .zap.long::after { + background-image: url("chrome://activity-stream/content/data/content/assets/long-zap.svg"); +} +.onboardingContainer .language-loader { + filter: invert(1); + margin-inline-end: 10px; + position: relative; + top: 3px; + width: 16px; + height: 16px; + margin-top: -6px; +} +@media (prefers-color-scheme: dark) { + .onboardingContainer .language-loader { + filter: invert(0); + } +} +.onboardingContainer .tiles-theme-container { + display: flex; + flex-direction: column; + align-items: center; + margin: 10px auto; +} +.onboardingContainer .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; +} +.onboardingContainer .sr-only.input { + height: 1px; + width: 1px; +} +.onboardingContainer .tiles-theme-section { + border: 0; + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: space-evenly; + margin-inline: 10px; + padding: 10px; + transition: var(--transition); +} +.onboardingContainer .tiles-theme-section:hover, .onboardingContainer .tiles-theme-section:active, .onboardingContainer .tiles-theme-section:focus-within { + border-radius: 8px; + outline: 2px solid var(--in-content-primary-button-background); +} +.onboardingContainer .tiles-theme-section .theme { + align-items: center; + display: flex; + flex-direction: column; + flex: 1; + padding: 0; + min-width: 50px; + width: 180px; + color: #000; + box-shadow: none; + border-radius: 4px; + cursor: pointer; + z-index: 0; +} +.onboardingContainer .tiles-theme-section .theme.colorway { + width: auto; +} +.onboardingContainer .tiles-theme-section .theme:focus, .onboardingContainer .tiles-theme-section .theme:active { + outline: initial; + outline-offset: initial; +} +.onboardingContainer .tiles-theme-section .theme .icon.colorway, +.onboardingContainer .tiles-theme-section .theme .label.colorway { + width: 20px; + height: 20px; +} +.onboardingContainer .tiles-theme-section .theme .icon { + background-size: cover; + width: 40px; + height: 40px; + border-radius: 40px; + outline: 1px solid var(--in-content-border-color); + outline-offset: -0.5px; + z-index: -1; +} +.onboardingContainer .tiles-theme-section .theme .icon:dir(rtl) { + transform: scaleX(-1); +} +.onboardingContainer .tiles-theme-section .theme .icon:focus, .onboardingContainer .tiles-theme-section .theme .icon:active, .onboardingContainer .tiles-theme-section .theme .icon.selected { + outline: 2px solid var(--in-content-primary-button-background); + outline-offset: 2px; +} +.onboardingContainer .tiles-theme-section .theme .icon.light { + background-image: url("resource://builtin-themes/light/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.dark { + background-image: url("resource://builtin-themes/dark/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.alpenglow { + background-image: url("resource://builtin-themes/alpenglow/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.default, .onboardingContainer .tiles-theme-section .theme .icon.automatic { + background-image: url("resource://default-theme/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.default.colorway, .onboardingContainer .tiles-theme-section .theme .icon.automatic.colorway { + background-image: url("chrome://activity-stream/content/data/content/assets/default.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.playmaker { + background-image: url("resource://builtin-themes/colorways/2022playmaker/balanced/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.expressionist { + background-image: url("resource://builtin-themes/colorways/2022expressionist/balanced/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.visionary { + background-image: url("resource://builtin-themes/colorways/2022visionary/balanced/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.dreamer { + background-image: url("resource://builtin-themes/colorways/2022dreamer/balanced/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.innovator { + background-image: url("resource://builtin-themes/colorways/2022innovator/balanced/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .icon.activist { + background-image: url("resource://builtin-themes/colorways/2022activist/balanced/icon.svg"); +} +.onboardingContainer .tiles-theme-section .theme .text { + display: flex; + color: var(--in-content-page-color); + font-size: 14px; + font-weight: normal; + line-height: 20px; + margin-inline-start: 0; + margin-top: 9px; +} +.onboardingContainer .tiles-theme-section legend { + cursor: default; +} +.onboardingContainer .tiles-container { + margin: 10px auto; +} +.onboardingContainer .tiles-container.info { + padding: 6px 12px 12px; +} +.onboardingContainer .tiles-container.info:hover, .onboardingContainer .tiles-container.info:focus { + background-color: rgba(217, 217, 227, 0.3); + border-radius: 4px; +} +.onboardingContainer .tiles-delayed { + animation: fadein 0.4s; +} +.onboardingContainer .multi-select-container { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-block: -1em 1em; + color: #5B5B66; + font-weight: 400; + font-size: 14px; + text-align: initial; + transition: var(--transition); + z-index: 1; +} +.onboardingContainer .multi-select-container .checkbox-container { + display: flex; + margin-bottom: 16px; +} +@media (prefers-color-scheme: dark) { + .onboardingContainer .multi-select-container { + color: #CFCFD8; + } +} +@media only screen and (max-width: 800px) { + .onboardingContainer .multi-select-container { + padding: 0 30px; + } +} +.onboardingContainer .mobile-downloads .qr-code-image { + margin: 24px 0 10px; + width: 113px; + height: 113px; +} +.onboardingContainer .mobile-downloads .email-link { + font-size: 16px; + font-weight: 400; + background: none; + text-decoration: underline; + cursor: pointer; + color: var(--in-content-link-color); +} +.onboardingContainer .mobile-downloads .email-link:hover { + text-decoration: none; + color: var(--in-content-link-color-hover); +} +.onboardingContainer .mobile-downloads .email-link:active { + text-decoration: none; + color: var(--in-content-link-color-active); +} +.onboardingContainer .mobile-downloads .email-link:hover { + background: none; +} +.onboardingContainer .mobile-downloads .ios button { + background-image: url("chrome://app-marketplace-icons/locale/ios.svg"); +} +.onboardingContainer .mobile-downloads .android button { + background-image: url("chrome://app-marketplace-icons/locale/android.png"); +} +.onboardingContainer .mobile-download-buttons { + list-style: none; + padding: 10px 0; + margin: 0; +} +.onboardingContainer .mobile-download-buttons li { + display: inline-block; +} +.onboardingContainer .mobile-download-buttons li button { + display: inline-block; + height: 45px; + width: 152px; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + box-shadow: none; + border: 0; +} +.onboardingContainer .mobile-download-buttons li:not(:first-child) { + margin-inline: 5px 0; +} +.onboardingContainer .dismiss-button { + position: absolute; + z-index: 1; + top: 0; + left: auto; + right: 0; + box-sizing: border-box; + padding: 0; + margin: 16px; + display: block; + float: inline-end; + background: url("chrome://global/skin/icons/close.svg") no-repeat center/16px; + height: 32px; + width: 32px; + align-self: end; + min-height: 32px; + min-width: 32px; + -moz-context-properties: fill; + fill: currentColor; + transition: var(--transition); +} +.onboardingContainer .dismiss-button:dir(rtl) { + left: 0; + right: auto; +} +@keyframes fadein { + from { + opacity: 0; + } +} +.onboardingContainer .secondary-cta { + display: flex; + align-items: end; + flex-direction: row; + justify-content: center; + font-size: 14px; + transition: var(--transition); +} +.onboardingContainer .secondary-cta.top { + justify-content: end; + padding-inline-end: min(150px, 500px - 70vh); + padding-top: 4px; + position: absolute; + top: 10px; + inset-inline-end: 20px; + z-index: 2; +} +.onboardingContainer .secondary-cta span { + color: var(--grey-subtitle-1); + margin: 0 4px; +} +.onboardingContainer .message-text, +.onboardingContainer .attrib-text { + transition: var(--transition); +} +.onboardingContainer .helptext { + padding: 1em; + text-align: center; + color: var(--grey-subtitle-1); + font-size: 12px; + line-height: 18px; +} +.onboardingContainer .helptext.default { + align-self: center; + max-width: 40%; +} +.onboardingContainer .helptext span { + padding-inline-end: 4px; +} +.onboardingContainer .helptext-img { + height: 1.5em; + width: 1.5em; + margin-inline-end: 4px; + vertical-align: middle; +} +.onboardingContainer .helptext-img.end { + margin: 4px; +} +.onboardingContainer .helptext-img.footer { + vertical-align: bottom; +} +.onboardingContainer .steps { + display: flex; + flex-direction: row; + justify-content: center; + margin-top: 0; + padding-block: 16px 0; + transition: var(--transition); + z-index: -1; + height: 48px; + box-sizing: border-box; +} +.onboardingContainer .steps.has-helptext { + padding-bottom: 0; +} +.onboardingContainer .steps .indicator { + width: 0; + height: 0; + margin-inline-end: 4px; + margin-inline-start: 4px; + background: var(--grey-subtitle-1); + border-radius: 5px; + border: 3px solid var(--in-content-button-text-color); + opacity: 0.35; + box-sizing: inherit; +} +.onboardingContainer .steps .indicator.current { + opacity: 1; + border-color: var(--checkbox-checked-bgcolor); +} +.onboardingContainer .steps .indicator.current:last-of-type:first-of-type { + opacity: 0; +} +.onboardingContainer .steps.progress-bar { + height: 6px; + padding-block: 0; + margin-block: 42px 0; + background-color: color-mix(in srgb, var(--in-content-button-text-color) 25%, transparent); + justify-content: start; + opacity: 1; + transition: none; +} +.onboardingContainer .steps.progress-bar .indicator { + width: 100%; + height: 100%; + margin-inline: -1px; + background-color: var(--checkbox-checked-bgcolor); + border: 0; + border-radius: 0; + opacity: 1; + transition: var(--progress-bar-transition); + translate: calc(var(--progress-bar-progress, 0%) - 100%); +} +.onboardingContainer .steps.progress-bar .indicator:dir(rtl) { + translate: calc(var(--progress-bar-progress, 0%) * -1 + 100%); +} +.onboardingContainer .additional-cta-container[flow] { + display: flex; + flex-flow: column wrap; + align-items: center; +} +.onboardingContainer .additional-cta-container[flow][flow=row] { + flex-direction: row; + justify-content: center; +} +.onboardingContainer .additional-cta-container[flow][flow=row] .secondary-cta { + flex-basis: 100%; +} +.onboardingContainer .primary, +.onboardingContainer .secondary, +.onboardingContainer .additional-cta { + font-size: 13px; + line-height: 16px; + padding: 11px 15px; + transition: var(--transition); +} +.onboardingContainer .primary.rtamo, +.onboardingContainer .secondary.rtamo, +.onboardingContainer .additional-cta.rtamo { + margin-top: 24px; +} +.onboardingContainer .secondary { + background-color: var(--in-content-button-background); + color: var(--in-content-button-text-color); +} +.onboardingContainer .noodle { + display: block; + background-repeat: no-repeat; + background-size: 100% 100%; + position: absolute; + transition: var(--transition); +} +.onboardingContainer .noodle:dir(rtl) { + scale: -1 1; +} +.onboardingContainer .outline-L { + background-image: url("chrome://activity-stream/content/data/content/assets/noodle-outline-L.svg"); +} +.onboardingContainer .solid-L { + background-image: url("chrome://activity-stream/content/data/content/assets/noodle-solid-L.svg"); + -moz-context-properties: fill; + fill: var(--in-content-page-background); + display: none; +} +.onboardingContainer .purple-C { + background-image: url("chrome://activity-stream/content/data/content/assets/noodle-C.svg"); + -moz-context-properties: fill; + fill: #E7258C; +} +.onboardingContainer .orange-L { + background-image: url("chrome://activity-stream/content/data/content/assets/noodle-solid-L.svg"); + -moz-context-properties: fill; + fill: #FFA437; +} +.onboardingContainer .screen-1 .section-main { + z-index: 1; + margin: auto; +} +.onboardingContainer .screen-1 .outline-L { + width: 87px; + height: 80px; + transform: rotate(10deg) translate(-30%, 200%); + transition-delay: 0.4s; + z-index: 2; +} +.onboardingContainer .screen-1 .orange-L { + width: 550px; + height: 660px; + transform: rotate(-155deg) translate(11%, -18%); + transition-delay: 0.2s; +} +.onboardingContainer .screen-1 .purple-C { + width: 310px; + height: 260px; + transform: translate(-18%, -67%); +} +.onboardingContainer .screen-1 .yellow-circle { + width: 165px; + height: 165px; + border-radius: 50%; + transform: translate(230%, -5%); + background: #952BB9; + transition-delay: -0.2s; +} +.onboardingContainer .dialog-initial .brand-logo { + transition-delay: 0.6s; +} +.onboardingContainer .dialog-initial .welcome-text { + transition-delay: 0.8s; +} +.onboardingContainer .dialog-initial .tiles-theme-section, +.onboardingContainer .dialog-initial .multi-select-container, +.onboardingContainer .dialog-initial migration-wizard { + transition-delay: 0.9s; +} +.onboardingContainer .dialog-initial .primary, +.onboardingContainer .dialog-initial .secondary, +.onboardingContainer .dialog-initial .secondary-cta, +.onboardingContainer .dialog-initial .steps, +.onboardingContainer .dialog-initial .cta-link { + transition-delay: 1s; +} +.onboardingContainer .screen:not(.dialog-initial) .tiles-theme-section, +.onboardingContainer .screen:not(.dialog-initial) .multi-select-container, +.onboardingContainer .screen:not(.dialog-initial) .colorway-text { + transition-delay: 0.2s; +} +.onboardingContainer .screen:not(.dialog-initial) .primary, +.onboardingContainer .screen:not(.dialog-initial) .secondary, +.onboardingContainer .screen:not(.dialog-initial) .secondary-cta, +.onboardingContainer .screen:not(.dialog-initial) .cta-link { + transition-delay: 0.4s; +} +.onboardingContainer .screen-2 .section-main { + z-index: 1; + margin: auto; +} +.onboardingContainer .screen-2 .outline-L { + width: 87px; + height: 80px; + transform: rotate(250deg) translate(-420%, 425%); + transition-delay: 0.2s; + z-index: 2; +} +.onboardingContainer .screen-2 .orange-L { + height: 800px; + width: 660px; + transform: rotate(35deg) translate(-10%, -7%); + transition-delay: -0.4s; +} +.onboardingContainer .screen-2 .purple-C { + width: 392px; + height: 394px; + transform: rotate(260deg) translate(-34%, -35%); + transition-delay: -0.2s; + fill: #952BB9; +} +.onboardingContainer .screen-2 .yellow-circle { + width: 165px; + height: 165px; + border-radius: 50%; + transform: translate(160%, 130%); + background: #E7258C; +} +.onboardingContainer.transition-in .noodle { + opacity: 0; + rotate: var(--rotate); + scale: var(--scale); +} +.onboardingContainer.transition-in .dialog-initial .main-content, +.onboardingContainer.transition-in .dialog-initial .dismiss-button { + translate: 0 calc(-2 * var(--translate)); +} +.onboardingContainer.transition-in .dialog-initial .brand-logo, +.onboardingContainer.transition-in .dialog-initial .steps { + opacity: 0; + translate: 0 calc(-1 * var(--translate)); +} +.onboardingContainer.transition-in .screen .welcome-text, +.onboardingContainer.transition-in .screen .multi-select-container, +.onboardingContainer.transition-in .screen .tiles-theme-section, +.onboardingContainer.transition-in .screen .colorway-text, +.onboardingContainer.transition-in .screen .primary, +.onboardingContainer.transition-in .screen .checkbox-container:not(.multi-select-item), +.onboardingContainer.transition-in .screen .secondary, +.onboardingContainer.transition-in .screen .secondary-cta:not(.top), +.onboardingContainer.transition-in .screen .cta-link, +.onboardingContainer.transition-in .screen migration-wizard { + opacity: 0; + translate: 0 calc(-1 * var(--translate)); +} +.onboardingContainer.transition-in .screen:not(.dialog-initial) .steps:not(.progress-bar) { + opacity: 0.2; +} +.onboardingContainer.transition-out .noodle { + opacity: 0; + rotate: var(--rotate); + scale: var(--scale); + transition-delay: 0.2s; +} +.onboardingContainer.transition-out .screen:not(.dialog-last) .main-content { + overflow: hidden; +} +.onboardingContainer.transition-out .screen:not(.dialog-last) .welcome-text, +.onboardingContainer.transition-out .screen:not(.dialog-last) .multi-select-container { + opacity: 0; + translate: 0 var(--translate); + transition-delay: 0.1s; +} +.onboardingContainer.transition-out .screen:not(.dialog-last) .tiles-theme-section, +.onboardingContainer.transition-out .screen:not(.dialog-last) .colorway-text, +.onboardingContainer.transition-out .screen:not(.dialog-last) migration-wizard { + opacity: 0; + translate: 0 var(--translate); + transition-delay: 0.2s; +} +.onboardingContainer.transition-out .screen:not(.dialog-last) .primary, +.onboardingContainer.transition-out .screen:not(.dialog-last) .checkbox-container:not(.multi-select-item), +.onboardingContainer.transition-out .screen:not(.dialog-last) .secondary, +.onboardingContainer.transition-out .screen:not(.dialog-last) .secondary-cta:not(.top), +.onboardingContainer.transition-out .screen:not(.dialog-last) .cta-link { + opacity: 0; + translate: 0 var(--translate); + transition-delay: 0.3s; +} +.onboardingContainer.transition-out .screen:not(.dialog-last) .steps:not(.progress-bar) { + opacity: 0.2; + transition-delay: 0.5s; +} +.onboardingContainer.transition-out .dialog-last .noodle { + transition-delay: 0s; +} +.onboardingContainer.transition-out .dialog-last .main-content, +.onboardingContainer.transition-out .dialog-last .dismiss-button { + opacity: 0; + translate: 0 calc(2 * var(--translate)); + transition-delay: 0.4s; +} +.onboardingContainer migration-wizard { + width: unset; + transition: var(--transition); +} +.onboardingContainer migration-wizard::part(buttons) { + margin-top: 32px; + justify-content: flex-start; +} +.onboardingContainer migration-wizard::part(deck) { + font-size: 0.83em; +} diff --git a/browser/components/newtab/aboutwelcome/content/aboutwelcome.html b/browser/components/newtab/aboutwelcome/content/aboutwelcome.html new file mode 100644 index 0000000000..a14b563057 --- /dev/null +++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm new file mode 100644 index 0000000000..0e17a0b3ae --- /dev/null +++ b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeDefaults.jsm @@ -0,0 +1,707 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const EXPORTED_SYMBOLS = ["AboutWelcomeDefaults"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", + AttributionCode: "resource:///modules/AttributionCode.sys.mjs", + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AWScreenUtils: "resource://activity-stream/lib/AWScreenUtils.jsm", +}); + +// Message to be updated based on finalized MR designs +const MR_ABOUT_WELCOME_DEFAULT = { + id: "MR_WELCOME_DEFAULT", + template: "multistage", + // Allow tests to easily disable transitions. + transitions: Services.prefs.getBoolPref( + "browser.aboutwelcome.transitions", + true + ), + backdrop: + "var(--mr-welcome-background-color) var(--mr-welcome-background-gradient)", + screens: [ + { + id: "AW_WELCOME_BACK", + targeting: "isDeviceMigration", + content: { + position: "split", + split_narrow_bkg_position: "-100px", + image_alt_text: { + string_id: "onboarding-device-migration-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/device-migration.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "onboarding-device-migration-title", + }, + subtitle: { + string_id: "onboarding-device-migration-subtitle", + }, + primary_button: { + label: { + string_id: "onboarding-device-migration-primary-button-label", + }, + action: { + type: "FXA_SIGNIN_FLOW", + navigate: "actionResult", + data: { + entrypoint: "fx-device-migration-onboarding", + extraParams: { + utm_content: "migration-onboarding", + utm_source: "fx-new-device-sync", + utm_medium: "firefox-desktop", + utm_campaign: "migration", + }, + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_EASY_SETUP", + targeting: + "os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-set-default-title", + }, + subtitle: { + string_id: "mr2022-onboarding-set-default-subtitle", + }, + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + defaultValue: true, + label: { + string_id: + "mr2022-onboarding-easy-setup-set-default-checkbox-label", + }, + action: { + type: "SET_DEFAULT_BROWSER", + }, + }, + { + id: "checkbox-2", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-easy-setup-import-checkbox-label", + }, + action: { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + }, + ], + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-easy-setup-primary-button-label", + }, + action: { + type: "MULTI_ACTION", + collectSelect: true, + navigate: true, + data: { + actions: [], + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label", + }, + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab", + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true, + }, + }, + }, + }, + { + id: "AW_PIN_FIREFOX", + targeting: + "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin)", + content: { + position: "split", + split_narrow_bkg_position: "-155px", + image_alt_text: { + string_id: "mr2022-onboarding-pin-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-pintaskbar.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-welcome-pin-header", + }, + subtitle: { + string_id: "mr2022-onboarding-welcome-pin-subtitle", + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-pin-primary-button-label", + }, + action: { + navigate: true, + type: "PIN_FIREFOX_TO_TASKBAR", + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + secondary_button_top: { + label: { + string_id: "mr1-onboarding-sign-in-button-label", + }, + action: { + data: { + entrypoint: "activity-stream-firstrun", + where: "tab", + }, + type: "SHOW_FIREFOX_ACCOUNTS", + addFlowParams: true, + }, + }, + }, + }, + { + id: "AW_LANGUAGE_MISMATCH", + content: { + position: "split", + background: "var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-live-language-text", + }, + subtitle: { + string_id: "mr2022-language-mismatch-subtitle", + }, + hero_text: { + string_id: "mr2022-onboarding-live-language-text", + useLangPack: true, + }, + languageSwitcher: { + downloading: { + string_id: "onboarding-live-language-button-label-downloading", + }, + cancel: { + string_id: "onboarding-live-language-secondary-cancel-download", + }, + waiting: { string_id: "onboarding-live-language-waiting-button" }, + skip: { string_id: "mr2022-onboarding-secondary-skip-button-label" }, + action: { + navigate: true, + }, + switch: { + string_id: "mr2022-onboarding-live-language-switch-to", + useLangPack: true, + }, + continue: { + string_id: "mr2022-onboarding-live-language-continue-in", + }, + }, + }, + }, + { + id: "AW_SET_DEFAULT", + targeting: + "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin)", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-set-default-title", + }, + subtitle: { + string_id: "mr2022-onboarding-set-default-subtitle", + }, + primary_button: { + label: { + string_id: "mr2022-onboarding-set-default-primary-button-label", + }, + action: { + navigate: true, + type: "SET_DEFAULT_BROWSER", + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_IMPORT_SETTINGS", + targeting: + "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin) && !useEmbeddedMigrationWizard", + content: { + position: "split", + split_narrow_bkg_position: "-42px", + image_alt_text: { + string_id: "mr2022-onboarding-import-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-import.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-import-header", + }, + subtitle: { + string_id: "mr2022-onboarding-import-subtitle", + }, + primary_button: { + label: { + string_id: + "mr2022-onboarding-import-primary-button-label-no-attribution", + }, + action: { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + navigate: true, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_IMPORT_SETTINGS_EMBEDDED", + targeting: + "!(os.windowsBuildNumber >= 15063 && !isDefaultBrowser && !doesAppNeedPin) && useEmbeddedMigrationWizard", + content: { + tiles: { type: "migration-wizard" }, + position: "split", + split_narrow_bkg_position: "-42px", + image_alt_text: { + string_id: "mr2022-onboarding-import-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-import.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + migrate_start: { + action: {}, + }, + migrate_close: { + action: { + navigate: true, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_MOBILE_DOWNLOAD", + // The mobile download screen should only be shown to users who + // are either not logged into FxA, or don't have any mobile devices syncing + targeting: "!isFxASignedIn || sync.mobileDevices == 0", + content: { + position: "split", + split_narrow_bkg_position: "-160px", + image_alt_text: { + string_id: "mr2022-onboarding-mobile-download-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-mobilecrosspromo.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-mobile-download-title", + }, + subtitle: { + string_id: "mr2022-onboarding-mobile-download-subtitle", + }, + hero_image: { + url: "chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user.svg", + }, + cta_paragraph: { + text: { + string_id: "mr2022-onboarding-mobile-download-cta-text", + string_name: "download-label", + }, + action: { + type: "OPEN_URL", + data: { + args: "https://www.mozilla.org/firefox/mobile/get-app/?utm_medium=firefox-desktop&utm_source=onboarding-modal&utm_campaign=mr2022&utm_content=new-global", + where: "tab", + }, + }, + }, + secondary_button: { + label: { + string_id: "mr2022-onboarding-secondary-skip-button-label", + }, + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + }, + { + id: "AW_GRATITUDE", + content: { + position: "split", + split_narrow_bkg_position: "-228px", + image_alt_text: { + string_id: "mr2022-onboarding-gratitude-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-gratitude.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: { + string_id: "mr2022-onboarding-gratitude-title", + }, + subtitle: { + string_id: "mr2022-onboarding-gratitude-subtitle", + }, + primary_button: { + label: { + string_id: "mr2-onboarding-start-browsing-button-label", + }, + action: { + navigate: true, + }, + }, + }, + }, + ], +}; + +async function getAddonFromRepository(data) { + const [addonInfo] = await lazy.AddonRepository.getAddonsByIDs([data]); + if (addonInfo.sourceURI.scheme !== "https") { + return null; + } + + return { + name: addonInfo.name, + url: addonInfo.sourceURI.spec, + iconURL: addonInfo.icons["64"] || addonInfo.icons["32"], + type: addonInfo.type, + screenshots: addonInfo.screenshots, + }; +} + +async function getAddonInfo(attrbObj) { + let { content, source } = attrbObj; + try { + if (!content || source !== "addons.mozilla.org") { + return null; + } + // Attribution data can be double encoded + while (content.includes("%")) { + try { + const result = decodeURIComponent(content); + if (result === content) { + break; + } + content = result; + } catch (e) { + break; + } + } + // return_to_amo embeds the addon id in the content + // param, prefixed with "rta:". Translating that + // happens in AddonRepository, however we can avoid + // an API call if we check up front here. + if (content.startsWith("rta:")) { + return await getAddonFromRepository(content); + } + } catch (e) { + console.error("Failed to get the latest add-on version for Return to AMO"); + } + return null; +} + +async function getAttributionContent() { + let attribution = await lazy.AttributionCode.getAttrDataAsync(); + if (attribution?.source === "addons.mozilla.org") { + let addonInfo = await getAddonInfo(attribution); + if (addonInfo) { + return { + ...addonInfo, + template: "return_to_amo", + }; + } + } + if (attribution?.ua) { + return { + ua: decodeURIComponent(attribution.ua), + }; + } + return null; +} + +// Return default multistage welcome content +function getDefaults() { + return Cu.cloneInto(MR_ABOUT_WELCOME_DEFAULT, {}); +} + +let gSourceL10n = null; + +// Localize Firefox download source from user agent attribution to show inside +// import primary button label such as 'Import from '. +// no firefox as import wizard doesn't show it +const allowedUAs = ["chrome", "edge", "ie"]; +function getLocalizedUA(ua) { + if (!gSourceL10n) { + gSourceL10n = new Localization(["browser/migration.ftl"]); + } + if (allowedUAs.includes(ua)) { + return gSourceL10n.formatValue(`source-name-${ua.toLowerCase()}`); + } + return null; +} + +// Function to evalute the appropriate string for the welcome screen button label +function evaluateWelcomeScreenButtonLabel(removeDefault) { + return removeDefault + ? "mr2022-onboarding-get-started-primary-button-label" + : "mr2022-onboarding-set-default-primary-button-label"; +} + +function prepareMobileDownload(content) { + let mobileContent = content?.screens?.find( + screen => screen.id === "AW_MOBILE_DOWNLOAD" + )?.content; + + if (!mobileContent) { + return content; + } + if (!lazy.BrowserUtils.sendToDeviceEmailsSupported()) { + // If send to device emails are not supported for a user's locale, + // remove the send to device link and update the screen text + delete mobileContent.cta_paragraph.action; + mobileContent.cta_paragraph.text = { + string_id: "mr2022-onboarding-no-mobile-download-cta-text", + }; + } + // Update CN specific QRCode url + if (AppConstants.isChinaRepack()) { + mobileContent.hero_image.url = `${mobileContent.hero_image.url.slice( + 0, + mobileContent.hero_image.url.indexOf(".svg") + )}-cn.svg`; + } + + return content; +} + +async function prepareContentForReact(content) { + const { screens } = content; + + if (content?.template === "return_to_amo") { + return content; + } + + // Change content for Windows 7 because non-light themes aren't quite right. + if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) { + await lazy.AWScreenUtils.removeScreens(screens, screen => + ["theme"].includes(screen.content?.tiles?.type) + ); + } + + // Set the primary import button source based on attribution. + if (content?.ua) { + // If available, add the browser source to action data + // and localized browser string args to primary button label + const { label, action } = + content?.screens?.find( + screen => + screen?.content?.primary_button?.action?.type === + "SHOW_MIGRATION_WIZARD" + )?.content?.primary_button ?? {}; + + if (action) { + action.data = { ...action.data, source: content.ua }; + } + + let browserStr = await getLocalizedUA(content.ua); + + if (label?.string_id) { + label.string_id = browserStr + ? "mr1-onboarding-import-primary-button-label-attribution" + : "mr2022-onboarding-import-primary-button-label-no-attribution"; + + label.args = browserStr ? { previous: browserStr } : {}; + } + } + + // If already pinned, convert "pin" screen to "welcome" with desired action. + let removeDefault = !content.needDefault; + if (!content.needPin) { + const pinScreen = content.screens?.find(screen => + screen.id?.startsWith("AW_PIN_FIREFOX") + ); + if (pinScreen?.content) { + pinScreen.id = removeDefault ? "AW_GET_STARTED" : "AW_ONLY_DEFAULT"; + pinScreen.content.title = { + string_id: "mr2022-onboarding-welcome-pin-header", + }; + + pinScreen.content.subtitle = { + string_id: removeDefault + ? "mr2022-onboarding-get-started-primary-subtitle" + : "mr2022-onboarding-set-default-only-subtitle", + }; + + pinScreen.content.primary_button = { + label: { + string_id: evaluateWelcomeScreenButtonLabel(removeDefault, content), + }, + action: { + navigate: true, + }, + }; + // Get started content will navigate without action, so remove "Not now." + if (!removeDefault) { + // The "pin" screen will now handle "default" so remove other "default." + pinScreen.content.primary_button.action.type = "SET_DEFAULT_BROWSER"; + removeDefault = true; + } + } + } + if (removeDefault) { + await lazy.AWScreenUtils.removeScreens(screens, screen => + screen.id?.startsWith("AW_SET_DEFAULT") + ); + } + + // Remove Firefox Accounts related UI and prevent related metrics. + if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled", false)) { + delete content.screens?.find( + screen => + screen.content?.secondary_button_top?.action?.type === + "SHOW_FIREFOX_ACCOUNTS" + )?.content.secondary_button_top; + content.skipFxA = true; + } + + // Remove the English-only image caption. + if (Services.locale.appLocaleAsBCP47.split("-")[0] !== "en") { + delete content.screens?.find( + screen => screen.content?.help_text?.deleteIfNotEn + )?.content.help_text; + } + + let shouldRemoveLanguageMismatchScreen = true; + if (content.languageMismatchEnabled) { + const screen = content?.screens?.find(s => s.id === "AW_LANGUAGE_MISMATCH"); + if (screen && content.appAndSystemLocaleInfo.canLiveReload) { + // Add the display names for the OS and Firefox languages, like "American English". + function addMessageArgs(obj) { + for (const value of Object.values(obj)) { + if (value?.string_id) { + value.args = content.appAndSystemLocaleInfo.displayNames; + } + } + } + + addMessageArgs(screen.content.languageSwitcher); + addMessageArgs(screen.content); + shouldRemoveLanguageMismatchScreen = false; + } + } + + if (shouldRemoveLanguageMismatchScreen) { + await lazy.AWScreenUtils.removeScreens( + screens, + screen => screen.id === "AW_LANGUAGE_MISMATCH" + ); + } + + return prepareMobileDownload(content); +} + +const AboutWelcomeDefaults = { + prepareContentForReact, + getDefaults, + getAttributionContent, +}; diff --git a/browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm new file mode 100644 index 0000000000..b8bdd44794 --- /dev/null +++ b/browser/components/newtab/aboutwelcome/lib/AboutWelcomeTelemetry.jsm @@ -0,0 +1,242 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const EXPORTED_SYMBOLS = ["AboutWelcomeTelemetry"]; +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AttributionCode: "resource:///modules/AttributionCode.sys.mjs", + ClientID: "resource://gre/modules/ClientID.sys.mjs", + TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + PingCentre: "resource:///modules/PingCentre.jsm", +}); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "structuredIngestionEndpointBase", + "browser.newtabpage.activity-stream.telemetry.structuredIngestion.endpoint", + "" +); +XPCOMUtils.defineLazyGetter(lazy, "telemetryClientId", () => + lazy.ClientID.getClientID() +); +XPCOMUtils.defineLazyGetter( + lazy, + "browserSessionId", + () => lazy.TelemetrySession.getMetadata("").sessionId +); + +XPCOMUtils.defineLazyGetter(lazy, "log", () => { + const { Logger } = ChromeUtils.importESModule( + "resource://messaging-system/lib/Logger.sys.mjs" + ); + return new Logger("AboutWelcomeTelemetry"); +}); + +const TELEMETRY_TOPIC = "about:welcome"; +const PING_TYPE = "onboarding"; +const PING_VERSION = "1"; +const STRUCTURED_INGESTION_NAMESPACE_MS = "messaging-system"; + +class AboutWelcomeTelemetry { + constructor() { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "telemetryEnabled", + "browser.newtabpage.activity-stream.telemetry", + false + ); + } + + /** + * Lazily initialize PingCentre for Activity Stream to send pings + */ + get pingCentre() { + Object.defineProperty(this, "pingCentre", { + value: new lazy.PingCentre({ topic: TELEMETRY_TOPIC }), + }); + return this.pingCentre; + } + + _generateStructuredIngestionEndpoint() { + const uuid = Services.uuid.generateUUID().toString(); + // Structured Ingestion does not support the UUID generated by Services.uuid, + // because it contains leading and trailing braces. Need to trim them first. + const docID = uuid.slice(1, -1); + const extension = `${STRUCTURED_INGESTION_NAMESPACE_MS}/${PING_TYPE}/${PING_VERSION}/${docID}`; + return `${lazy.structuredIngestionEndpointBase}/${extension}`; + } + + /** + * Attach browser attribution data to a ping payload. + * + * It intentionally queries the *cached* attribution data other than calling + * `getAttrDataAsync()` in order to minimize the overhead here. + * For the same reason, we are not querying the attribution data from + * `TelemetryEnvironment.currentEnvironment.settings`. + * + * In practice, it's very likely that the attribution data is already read + * and cached at some point by `AboutWelcomeParent`, so it should be able to + * read the cached results for the most if not all of the pings. + */ + _maybeAttachAttribution(ping) { + const attribution = lazy.AttributionCode.getCachedAttributionData(); + if (attribution && Object.keys(attribution).length) { + ping.attribution = attribution; + } + return ping; + } + + async _createPing(event) { + if (event.event_context && typeof event.event_context === "object") { + event.event_context = JSON.stringify(event.event_context); + } + let ping = { + ...event, + addon_version: Services.appinfo.appBuildID, + locale: Services.locale.appLocaleAsBCP47, + client_id: await lazy.telemetryClientId, + browser_session_id: lazy.browserSessionId, + }; + + return this._maybeAttachAttribution(ping); + } + + /** + * Augment the provided event with some metadata and then send it + * to the messaging-system's onboarding endpoint. + * + * Is sometimes used by non-onboarding events. + * + * @param event - an object almost certainly from an onboarding flow (though + * there is a case where spotlight may use this, too) + * containing a nested structure of data for reporting as + * telemetry, as documented in + * https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/data_events.html + * Does not have all of its data (`_createPing` will augment + * with ids and attribution if available). + */ + async sendTelemetry(event) { + if (!this.telemetryEnabled) { + return; + } + + const ping = await this._createPing(event); + + try { + this.submitGleanPingForPing(ping); + } catch (e) { + // Though Glean APIs are forbidden to throw, it may be possible that a + // mismatch between the shape of `ping` and the defined metrics is not + // adequately handled. + Glean.messagingSystem.gleanPingForPingFailures.add(1); + } + + this.pingCentre.sendStructuredIngestionPing( + ping, + this._generateStructuredIngestionEndpoint(), + STRUCTURED_INGESTION_NAMESPACE_MS + ); + } + + /** + * Tries to infer appropriate Glean metrics on the "messaging-system" ping, + * sets them, and submits a "messaging-system" ping. + * + * Does not check if telemetry is enabled. + * (Though Glean will check the global prefs). + * + * Note: This is a very unusual use of Glean that is specific to the use- + * cases of Messaging System. Please do not copy this pattern. + */ + submitGleanPingForPing(ping) { + lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`); + // event.event_context is an object, but it may have been stringified. + let event_context = ping?.event_context; + if (typeof event_context === "string") { + try { + event_context = JSON.parse(event_context); + } catch (e) { + Glean.messagingSystem.eventContextParseError.add(1); + } + } + + // We echo certain properties from event_context into their own metrics + // to aid analysis. + if (event_context?.reason) { + Glean.messagingSystem.eventReason.set(event_context.reason); + } + if (event_context?.page) { + Glean.messagingSystem.eventPage.set(event_context.page); + } + if (event_context?.source) { + Glean.messagingSystem.eventSource.set(event_context.source); + } + + // The event_context is also provided as-is as stringified JSON. + if (event_context) { + Glean.messagingSystem.eventContext.set(JSON.stringify(event_context)); + } + + if ("attribution" in ping) { + for (const [key, value] of Object.entries(ping.attribution)) { + const camelKey = this._snakeToCamelCase(key); + try { + Glean.messagingSystemAttribution[camelKey].set(value); + } catch (e) { + // We here acknowledge that we don't know the full breadth of data + // being collected. Ideally AttributionCode will later centralize + // definition and reporting of attribution data and we can be rid of + // this fail-safe for collecting the names of unknown keys. + Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1); + } + } + } + + // List of keys handled above. + const handledKeys = ["event_context", "attribution"]; + + for (const [key, value] of Object.entries(ping)) { + if (handledKeys.includes(key)) { + continue; + } + const camelKey = this._snakeToCamelCase(key); + try { + // We here acknowledge that even known keys might have non-scalar + // values. We're pretty sure we handled them all with handledKeys, + // but we might not have. + // Ideally this can later be removed after running for a version or two + // with no values seen in messaging_system.invalid_nested_data + if (typeof value === "object") { + Glean.messagingSystem.invalidNestedData[camelKey].add(1); + } else { + Glean.messagingSystem[camelKey].set(value); + } + } catch (e) { + // We here acknowledge that we don't know the full breadth of data being + // collected. Ideally we will later gain that confidence and can remove + // this fail-safe for collecting the names of unknown keys. + Glean.messagingSystem.unknownKeys[camelKey].add(1); + // TODO(bug 1600008): For testing, also record the overall count. + Glean.messagingSystem.unknownKeyCount.add(1); + } + } + + // With all the metrics set, now it's time to submit this ping. + GleanPings.messagingSystem.submit(); + } + + _snakeToCamelCase(s) { + return s.toString().replace(/_([a-z])/gi, (_str, group) => { + return group.toUpperCase(); + }); + } +} diff --git a/browser/components/newtab/actors/ASRouterChild.sys.mjs b/browser/components/newtab/actors/ASRouterChild.sys.mjs new file mode 100644 index 0000000000..bd2c030fab --- /dev/null +++ b/browser/components/newtab/actors/ASRouterChild.sys.mjs @@ -0,0 +1,111 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + MESSAGE_TYPE_LIST, + MESSAGE_TYPE_HASH as msg, +} from "resource://activity-stream/common/ActorConstants.sys.mjs"; + +const VALID_TYPES = new Set(MESSAGE_TYPE_LIST); + +export class ASRouterChild extends JSWindowActorChild { + constructor() { + super(); + this.observers = new Set(); + } + + didDestroy() { + this.observers.clear(); + } + + actorCreated() { + // NOTE: DOMDocElementInserted may be called multiple times per + // PWindowGlobal due to the initial about:blank document's window global + // being re-used. + const window = this.contentWindow; + Cu.exportFunction(this.asRouterMessage.bind(this), window, { + defineAs: "ASRouterMessage", + }); + Cu.exportFunction(this.addParentListener.bind(this), window, { + defineAs: "ASRouterAddParentListener", + }); + Cu.exportFunction(this.removeParentListener.bind(this), window, { + defineAs: "ASRouterRemoveParentListener", + }); + } + + handleEvent(event) { + // DOMDocElementCreated is only used to create the actor. + } + + addParentListener(listener) { + this.observers.add(listener); + } + + removeParentListener(listener) { + this.observers.delete(listener); + } + + receiveMessage({ name, data }) { + switch (name) { + case "EnterSnippetsPreviewMode": + case "UpdateAdminState": + case "ClearProviders": + case "ClearMessages": { + this.observers.forEach(listener => { + let result = Cu.cloneInto( + { + type: name, + data, + }, + this.contentWindow + ); + listener(result); + }); + break; + } + } + } + + wrapPromise(promise) { + return new this.contentWindow.Promise((resolve, reject) => + promise.then(resolve, reject) + ); + } + + sendQuery(aName, aData = null) { + return this.wrapPromise( + new Promise(resolve => { + super.sendQuery(aName, aData).then(result => { + resolve(Cu.cloneInto(result, this.contentWindow)); + }); + }) + ); + } + + asRouterMessage({ type, data }) { + if (VALID_TYPES.has(type)) { + switch (type) { + case msg.DISABLE_PROVIDER: + case msg.ENABLE_PROVIDER: + case msg.EXPIRE_QUERY_CACHE: + case msg.FORCE_WHATSNEW_PANEL: + case msg.CLOSE_WHATSNEW_PANEL: + case msg.FORCE_PRIVATE_BROWSING_WINDOW: + case msg.IMPRESSION: + case msg.RESET_PROVIDER_PREF: + case msg.SET_PROVIDER_USER_PREF: + case msg.USER_ACTION: { + return this.sendAsyncMessage(type, data); + } + default: { + // these messages need a response + return this.sendQuery(type, data); + } + } + } + throw new Error(`Unexpected type "${type}"`); + } +} diff --git a/browser/components/newtab/actors/ASRouterParent.sys.mjs b/browser/components/newtab/actors/ASRouterParent.sys.mjs new file mode 100644 index 0000000000..9c785faaa5 --- /dev/null +++ b/browser/components/newtab/actors/ASRouterParent.sys.mjs @@ -0,0 +1,103 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { MESSAGE_TYPE_HASH } from "resource://activity-stream/common/ActorConstants.sys.mjs"; + +import { ASRouterNewTabHook } from "resource://activity-stream/lib/ASRouterNewTabHook.sys.mjs"; + +const { ASRouterDefaultConfig } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouterDefaultConfig.jsm" +); + +export class ASRouterTabs { + constructor({ asRouterNewTabHook }) { + this.actors = new Set(); + this.destroy = () => {}; + asRouterNewTabHook.createInstance(ASRouterDefaultConfig()); + this.loadingMessageHandler = asRouterNewTabHook + .getInstance() + .then(initializer => { + const parentProcessMessageHandler = initializer.connect({ + clearChildMessages: ids => this.messageAll("ClearMessages", ids), + clearChildProviders: ids => this.messageAll("ClearProviders", ids), + updateAdminState: state => this.messageAll("UpdateAdminState", state), + }); + this.destroy = () => { + initializer.disconnect(); + }; + return parentProcessMessageHandler; + }); + } + + get size() { + return this.actors.size; + } + + messageAll(message, data) { + return Promise.all( + [...this.actors].map(a => a.sendAsyncMessage(message, data)) + ); + } + + registerActor(actor) { + this.actors.add(actor); + } + + unregisterActor(actor) { + this.actors.delete(actor); + } +} + +const defaultTabsFactory = () => + new ASRouterTabs({ asRouterNewTabHook: ASRouterNewTabHook }); + +export class ASRouterParent extends JSWindowActorParent { + static tabs = null; + + static nextTabId = 0; + + constructor({ tabsFactory } = { tabsFactory: defaultTabsFactory }) { + super(); + this.tabsFactory = tabsFactory; + } + + actorCreated() { + ASRouterParent.tabs = ASRouterParent.tabs || this.tabsFactory(); + this.tabsFactory = null; + this.tabId = ++ASRouterParent.nextTabId; + ASRouterParent.tabs.registerActor(this); + } + + didDestroy() { + ASRouterParent.tabs.unregisterActor(this); + if (ASRouterParent.tabs.size < 1) { + ASRouterParent.tabs.destroy(); + ASRouterParent.tabs = null; + } + } + + getTab() { + return { + id: this.tabId, + browser: this.browsingContext.embedderElement, + }; + } + + receiveMessage({ name, data }) { + return ASRouterParent.tabs.loadingMessageHandler.then(handler => { + if (name === MESSAGE_TYPE_HASH.BLOCK_MESSAGE_BY_ID) { + return Promise.all([ + handler.handleMessage(name, data, this.getTab()), + // All tabs should clear messages not just preloaded, for example + // two different windows can display the same snippet. + // ASRouter blocks snippets by campaign not by id so we just tell + // other tabs that this specific campaign was blocked. + ASRouterParent.tabs.messageAll("ClearMessages", [data.campaign]), + ]).then(([handleMessageResult]) => handleMessageResult); + } + return handler.handleMessage(name, data, this.getTab()); + }); + } +} diff --git a/browser/components/newtab/bin/render-activity-stream-html.js b/browser/components/newtab/bin/render-activity-stream-html.js new file mode 100644 index 0000000000..d87c096b4c --- /dev/null +++ b/browser/components/newtab/bin/render-activity-stream-html.js @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ +const fs = require("fs"); +const { mkdir } = require("shelljs"); +const path = require("path"); + +// Note: DEFAULT_OPTIONS.baseUrl should match BASE_URL in aboutNewTabService.js +// in mozilla-central. +const DEFAULT_OPTIONS = { + addonPath: "..", + baseUrl: "resource://activity-stream/", +}; + +/** + * templateHTML - Generates HTML for activity stream, given some options and + * prerendered HTML if necessary. + * + * @param {obj} options + * {str} options.baseUrl The base URL for all local assets + * {bool} options.debug Should we use dev versions of JS libraries? + * {bool} options.noscripts Should we include scripts in the prerendered files? + * @return {str} An HTML document as a string + */ +function templateHTML(options) { + const debugString = options.debug ? "-dev" : ""; + // This list must match any similar ones in AboutNewTabService.jsm. + const scripts = [ + "chrome://browser/content/contentSearchUI.js", + "chrome://browser/content/contentSearchHandoffUI.js", + "chrome://browser/content/contentTheme.js", + `${options.baseUrl}vendor/react${debugString}.js`, + `${options.baseUrl}vendor/react-dom${debugString}.js`, + `${options.baseUrl}vendor/prop-types.js`, + `${options.baseUrl}vendor/redux.js`, + `${options.baseUrl}vendor/react-redux.js`, + `${options.baseUrl}vendor/react-transition-group.js`, + `${options.baseUrl}data/content/activity-stream.bundle.js`, + `${options.baseUrl}data/content/newtab-render.js`, + ]; + + // Add spacing and script tags + const scriptRender = `\n${scripts + .map(script => ` `) + .join("\n")}`; + + // The markup below needs to be formatted by Prettier. But any diff after + // running this script should be caught by try-runnner.js + return ` + + + + + + + + + + + + + + + + + +
+ ${ + options.noscripts ? "" : scriptRender + } + + +`.trimLeft(); +} + +/** + * writeFiles - Writes to the desired files the result of a template given + * various prerendered data and options. + * + * @param {string} destPath Path to write the files to + * @param {Map} filesMap Mapping of a string file name to templater + * @param {Object} options Various options for the templater + */ +function writeFiles(destPath, filesMap, options) { + for (const [file, templater] of filesMap) { + fs.writeFileSync(path.join(destPath, file), templater({ options })); + console.log("\x1b[32m", `✓ ${file}`, "\x1b[0m"); + } +} + +const STATIC_FILES = new Map([ + ["activity-stream.html", ({ options }) => templateHTML(options)], + [ + "activity-stream-debug.html", + ({ options }) => templateHTML(Object.assign({}, options, { debug: true })), + ], + [ + "activity-stream-noscripts.html", + ({ options }) => + templateHTML(Object.assign({}, options, { noscripts: true })), + ], +]); + +/** + * main - Parses command line arguments, generates html and js with templates, + * and writes files to their specified locations. + */ +function main() { + // eslint-disable-line max-statements + // This code parses command line arguments passed to this script. + // Note: process.argv.slice(2) is necessary because the first two items in + // process.argv are paths + const args = require("minimist")(process.argv.slice(2), { + alias: { + addonPath: "a", + baseUrl: "b", + }, + }); + + const options = Object.assign({ debug: false }, DEFAULT_OPTIONS, args || {}); + const addonPath = path.resolve(__dirname, options.addonPath); + const prerenderedPath = path.join(addonPath, "prerendered"); + console.log(`Writing prerendered files to ${prerenderedPath}:`); + + mkdir("-p", prerenderedPath); + writeFiles(prerenderedPath, STATIC_FILES, options); +} + +main(); diff --git a/browser/components/newtab/bin/try-runner.js b/browser/components/newtab/bin/try-runner.js new file mode 100644 index 0000000000..78ee2c1be4 --- /dev/null +++ b/browser/components/newtab/bin/try-runner.js @@ -0,0 +1,193 @@ +/* eslint-disable no-console */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +/* + * A small test runner/reporter for node-based tests, + * which are run via taskcluster node(debugger). + * + * Forked from + * https://searchfox.org/mozilla-central/rev/c3453c7a0427eb27d467e1582f821f402aed9850/devtools/client/debugger/bin/try-runner.js + */ + +const { execFileSync } = require("child_process"); +const { readFileSync } = require("fs"); +const path = require("path"); + +function logErrors(tool, errors) { + for (const error of errors) { + console.log(`TEST-UNEXPECTED-FAIL ${tool} | ${error}`); + } + return errors; +} + +function execOut(...args) { + let exitCode = 0; + let out; + let err; + + try { + out = execFileSync(...args, { + silent: false, + }); + } catch (e) { + // For debugging on (eg) try server... + // + // if (e) { + // logErrors("execOut", ["execFileSync returned exception: ", e]); + // } + + out = e && e.stdout; + err = e && e.stderr; + exitCode = e && e.status; + } + return { exitCode, out: out && out.toString(), err: err && err.toString() }; +} + +function logStart(name) { + console.log(`TEST START | ${name}`); +} + +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; + +function checkBundles() { + logStart("checkBundles"); + + const items = { + "Activity Stream bundle": { + path: path.join("data", "content", "activity-stream.bundle.js"), + }, + "activity-stream.html": { + path: path.join("prerendered", "activity-stream.html"), + }, + "activity-stream-debug.html": { + path: path.join("prerendered", "activity-stream-debug.html"), + }, + "activity-stream-noscripts.html": { + path: path.join("prerendered", "activity-stream-noscripts.html"), + }, + "activity-stream-linux.css": { + path: path.join("css", "activity-stream-linux.css"), + }, + "activity-stream-mac.css": { + path: path.join("css", "activity-stream-mac.css"), + }, + "activity-stream-windows.css": { + path: path.join("css", "activity-stream-windows.css"), + }, + "About:welcome bundle": { + path: path.join("aboutwelcome", "content", "aboutwelcome.bundle.js"), + }, + "aboutwelcome.css": { + path: path.join("aboutwelcome", "content", "aboutwelcome.css"), + }, + }; + const errors = []; + + for (const name of Object.keys(items)) { + const item = items[name]; + item.before = readFileSync(item.path, item.encoding || "utf8"); + } + + let bundleExitCode = execOut(npmCommand, ["run", "bundle"]).exitCode; + + for (const name of Object.keys(items)) { + const item = items[name]; + const after = readFileSync(item.path, item.encoding || "utf8"); + + if (item.before !== after) { + errors.push(`${name} out of date`); + } + } + + if (bundleExitCode !== 0) { + errors.push("npm:bundle did not run successfully"); + } + + logErrors("checkBundles", errors); + return errors.length === 0; +} + +function karma() { + logStart("karma"); + + const errors = []; + const { exitCode, out } = execOut(npmCommand, [ + "run", + "testmc:unit", + // , "--", "--log-level", "--verbose", + // to debug the karma integration, uncomment the above line + ]); + + // karma spits everything to stdout, not stderr, so if nothing came back on + // stdout, give up now. + if (!out) { + return false; + } + + // Detect mocha failures + let jsonContent; + try { + // Note that this will be overwritten at each run, but that shouldn't + // matter. + jsonContent = readFileSync(path.join("logs", "karma-run-results.json")); + } catch (ex) { + console.error("exception reading karma-run-results.json: ", ex); + return false; + } + const results = JSON.parse(jsonContent); + // eslint-disable-next-line guard-for-in + for (let testArray in results.result) { + let failedTests = Array.from(results.result[testArray]).filter( + test => !test.success && !test.skipped + ); + + errors.push( + ...failedTests.map( + test => `${test.suite.join(":")} ${test.description}: ${test.log[0]}` + ) + ); + } + + // Detect istanbul failures (coverage thresholds set in karma config) + const coverage = out.match(/ERROR.+coverage-istanbul.+/g); + if (coverage) { + errors.push(...coverage.map(line => line.match(/Coverage.+/)[0])); + } + + logErrors("karma", errors); + + console.log("-----karma stdout below this line---"); + console.log(out); + console.log("-----karma stdout above this line---"); + + // Pass if there's no detected errors and nothing unexpected. + return errors.length === 0 && !exitCode; +} + +function zipCodeCoverage() { + logStart("zipCodeCoverage"); + const { exitCode, out } = execOut("zip", [ + "-j", + "logs/coverage/code-coverage-grcov", + "logs/coverage/lcov.info", + ]); + + console.log("zipCodeCoverage log output: ", out); + + if (!exitCode) { + return true; + } + + return false; +} + +const tests = {}; +const success = [checkBundles, karma, zipCodeCoverage].every( + t => (tests[t.name] = t()) +); +console.log(tests); + +process.exitCode = success ? 0 : 1; +console.log("CODE", process.exitCode); diff --git a/browser/components/newtab/bin/vendor.js b/browser/components/newtab/bin/vendor.js new file mode 100644 index 0000000000..3d929dcf4b --- /dev/null +++ b/browser/components/newtab/bin/vendor.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ + +const { cp, set } = require("shelljs"); +const path = require("path"); + +const filesToVendor = { + // XXX currently these two licenses are identical. Perhaps we should check + // in case that changes at some point in the future. + "react/LICENSE": "REACT_AND_REACT_DOM_LICENSE", + "react/umd/react.production.min.js": "react.js", + "react/umd/react.development.js": "react-dev.js", + "react-dom/umd/react-dom.production.min.js": "react-dom.js", + "react-dom/umd/react-dom.development.js": "react-dom-dev.js", + "react-dom/umd/react-dom-server.browser.production.min.js": + "react-dom-server.js", + "react-redux/LICENSE.md": "REACT_REDUX_LICENSE", + "react-redux/dist/react-redux.min.js": "react-redux.js", + "react-transition-group/dist/react-transition-group.min.js": + "react-transition-group.js", + "react-transition-group/LICENSE": "REACT_TRANSITION_GROUP_LICENSE", +}; + +set("-v"); // Echo all the copy commands so the user can see what's going on +for (let srcPath of Object.keys(filesToVendor)) { + cp( + path.join("node_modules", srcPath), + path.join("vendor", filesToVendor[srcPath]) + ); +} + +console.log(` +Check to see if any license files have changed, and, if so, be sure to update +https://searchfox.org/mozilla-central/source/toolkit/content/license.html`); diff --git a/browser/components/newtab/common/Actions.sys.mjs b/browser/components/newtab/common/Actions.sys.mjs new file mode 100644 index 0000000000..16dfd5dabf --- /dev/null +++ b/browser/components/newtab/common/Actions.sys.mjs @@ -0,0 +1,459 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; +export const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; +export const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; +export const UI_CODE = 1; +export const BACKGROUND_PROCESS = 2; + +/** + * globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process? + * Use this in action creators if you need different logic + * for ui/background processes. + */ +export const globalImportContext = + typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE; + +// Create an object that avoids accidental differing key/value pairs: +// { +// INIT: "INIT", +// UNINIT: "UNINIT" +// } +export const actionTypes = {}; + +for (const type of [ + "ABOUT_SPONSORED_TOP_SITES", + "ADDONS_INFO_REQUEST", + "ADDONS_INFO_RESPONSE", + "ARCHIVE_FROM_POCKET", + "AS_ROUTER_INITIALIZED", + "AS_ROUTER_PREF_CHANGED", + "AS_ROUTER_TARGETING_UPDATE", + "AS_ROUTER_TELEMETRY_USER_EVENT", + "BLOCK_URL", + "BOOKMARK_URL", + "CLEAR_PREF", + "COPY_DOWNLOAD_LINK", + "DELETE_BOOKMARK_BY_ID", + "DELETE_FROM_POCKET", + "DELETE_HISTORY_URL", + "DIALOG_CANCEL", + "DIALOG_OPEN", + "DISABLE_SEARCH", + "DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE", + "DISCOVERY_STREAM_CONFIG_CHANGE", + "DISCOVERY_STREAM_CONFIG_RESET", + "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", + "DISCOVERY_STREAM_CONFIG_SETUP", + "DISCOVERY_STREAM_CONFIG_SET_VALUE", + "DISCOVERY_STREAM_DEV_EXPIRE_CACHE", + "DISCOVERY_STREAM_DEV_IDLE_DAILY", + "DISCOVERY_STREAM_DEV_SYNC_RS", + "DISCOVERY_STREAM_DEV_SYSTEM_TICK", + "DISCOVERY_STREAM_EXPERIMENT_DATA", + "DISCOVERY_STREAM_FEEDS_UPDATE", + "DISCOVERY_STREAM_FEED_UPDATE", + "DISCOVERY_STREAM_IMPRESSION_STATS", + "DISCOVERY_STREAM_LAYOUT_RESET", + "DISCOVERY_STREAM_LAYOUT_UPDATE", + "DISCOVERY_STREAM_LINK_BLOCKED", + "DISCOVERY_STREAM_LOADED_CONTENT", + "DISCOVERY_STREAM_PERSONALIZATION_INIT", + "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", + "DISCOVERY_STREAM_PERSONALIZATION_TOGGLE", + "DISCOVERY_STREAM_POCKET_STATE_INIT", + "DISCOVERY_STREAM_POCKET_STATE_SET", + "DISCOVERY_STREAM_PREFS_SETUP", + "DISCOVERY_STREAM_RECENT_SAVES", + "DISCOVERY_STREAM_RETRY_FEED", + "DISCOVERY_STREAM_SPOCS_CAPS", + "DISCOVERY_STREAM_SPOCS_ENDPOINT", + "DISCOVERY_STREAM_SPOCS_PLACEMENTS", + "DISCOVERY_STREAM_SPOCS_UPDATE", + "DISCOVERY_STREAM_SPOC_BLOCKED", + "DISCOVERY_STREAM_SPOC_IMPRESSION", + "DISCOVERY_STREAM_USER_EVENT", + "DOWNLOAD_CHANGED", + "FAKE_FOCUS_SEARCH", + "FILL_SEARCH_TERM", + "HANDOFF_SEARCH_TO_AWESOMEBAR", + "HIDE_PERSONALIZE", + "HIDE_PRIVACY_INFO", + "INIT", + "NEW_TAB_INIT", + "NEW_TAB_INITIAL_STATE", + "NEW_TAB_LOAD", + "NEW_TAB_REHYDRATED", + "NEW_TAB_STATE_REQUEST", + "NEW_TAB_UNLOAD", + "OPEN_DOWNLOAD_FILE", + "OPEN_LINK", + "OPEN_NEW_WINDOW", + "OPEN_PRIVATE_WINDOW", + "OPEN_WEBEXT_SETTINGS", + "PARTNER_LINK_ATTRIBUTION", + "PLACES_BOOKMARKS_REMOVED", + "PLACES_BOOKMARK_ADDED", + "PLACES_HISTORY_CLEARED", + "PLACES_LINKS_CHANGED", + "PLACES_LINKS_DELETED", + "PLACES_LINK_BLOCKED", + "PLACES_SAVED_TO_POCKET", + "POCKET_CTA", + "POCKET_LINK_DELETED_OR_ARCHIVED", + "POCKET_LOGGED_IN", + "POCKET_WAITING_FOR_SPOC", + "PREFS_INITIAL_VALUES", + "PREF_CHANGED", + "PREVIEW_REQUEST", + "PREVIEW_REQUEST_CANCEL", + "PREVIEW_RESPONSE", + "REMOVE_DOWNLOAD_FILE", + "RICH_ICON_MISSING", + "SAVE_SESSION_PERF_DATA", + "SAVE_TO_POCKET", + "SCREENSHOT_UPDATED", + "SECTION_DEREGISTER", + "SECTION_DISABLE", + "SECTION_ENABLE", + "SECTION_MOVE", + "SECTION_OPTIONS_CHANGED", + "SECTION_REGISTER", + "SECTION_UPDATE", + "SECTION_UPDATE_CARD", + "SETTINGS_CLOSE", + "SETTINGS_OPEN", + "SET_PREF", + "SHOW_DOWNLOAD_FILE", + "SHOW_FIREFOX_ACCOUNTS", + "SHOW_PERSONALIZE", + "SHOW_PRIVACY_INFO", + "SHOW_SEARCH", + "SKIPPED_SIGNIN", + "SNIPPETS_BLOCKLIST_CLEARED", + "SNIPPETS_BLOCKLIST_UPDATED", + "SNIPPETS_DATA", + "SNIPPETS_PREVIEW_MODE", + "SNIPPETS_RESET", + "SNIPPET_BLOCKED", + "SUBMIT_EMAIL", + "SUBMIT_SIGNIN", + "SYSTEM_TICK", + "TELEMETRY_IMPRESSION_STATS", + "TELEMETRY_USER_EVENT", + "TOP_SITES_CANCEL_EDIT", + "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", + "TOP_SITES_EDIT", + "TOP_SITES_INSERT", + "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", + "TOP_SITES_ORGANIC_IMPRESSION_STATS", + "TOP_SITES_PIN", + "TOP_SITES_PREFS_UPDATED", + "TOP_SITES_SPONSORED_IMPRESSION_STATS", + "TOP_SITES_UNPIN", + "TOP_SITES_UPDATED", + "TOTAL_BOOKMARKS_REQUEST", + "TOTAL_BOOKMARKS_RESPONSE", + "UNINIT", + "UPDATE_PINNED_SEARCH_SHORTCUTS", + "UPDATE_SEARCH_SHORTCUTS", + "UPDATE_SECTION_PREFS", + "WEBEXT_CLICK", + "WEBEXT_DISMISS", +]) { + actionTypes[type] = type; +} + +// Helper function for creating routed actions between content and main +// Not intended to be used by consumers +function _RouteMessage(action, options) { + const meta = action.meta ? { ...action.meta } : {}; + if (!options || !options.from || !options.to) { + throw new Error( + "Routed Messages must have options as the second parameter, and must at least include a .from and .to property." + ); + } + // For each of these fields, if they are passed as an option, + // add them to the action. If they are not defined, remove them. + ["from", "to", "toTarget", "fromTarget", "skipMain", "skipLocal"].forEach( + o => { + if (typeof options[o] !== "undefined") { + meta[o] = options[o]; + } else if (meta[o]) { + delete meta[o]; + } + } + ); + return { ...action, meta }; +} + +/** + * AlsoToMain - Creates a message that will be dispatched locally and also sent to the Main process. + * + * @param {object} action Any redux action (required) + * @param {object} options + * @param {bool} skipLocal Used by OnlyToMain to skip the main reducer + * @param {string} fromTarget The id of the content port from which the action originated. (optional) + * @return {object} An action with added .meta properties + */ +function AlsoToMain(action, fromTarget, skipLocal) { + return _RouteMessage(action, { + from: CONTENT_MESSAGE_TYPE, + to: MAIN_MESSAGE_TYPE, + fromTarget, + skipLocal, + }); +} + +/** + * OnlyToMain - Creates a message that will be sent to the Main process and skip the local reducer. + * + * @param {object} action Any redux action (required) + * @param {object} options + * @param {string} fromTarget The id of the content port from which the action originated. (optional) + * @return {object} An action with added .meta properties + */ +function OnlyToMain(action, fromTarget) { + return AlsoToMain(action, fromTarget, true); +} + +/** + * BroadcastToContent - Creates a message that will be dispatched to main and sent to ALL content processes. + * + * @param {object} action Any redux action (required) + * @return {object} An action with added .meta properties + */ +function BroadcastToContent(action) { + return _RouteMessage(action, { + from: MAIN_MESSAGE_TYPE, + to: CONTENT_MESSAGE_TYPE, + }); +} + +/** + * AlsoToOneContent - Creates a message that will be will be dispatched to the main store + * and also sent to a particular Content process. + * + * @param {object} action Any redux action (required) + * @param {string} target The id of a content port + * @param {bool} skipMain Used by OnlyToOneContent to skip the main process + * @return {object} An action with added .meta properties + */ +function AlsoToOneContent(action, target, skipMain) { + if (!target) { + throw new Error( + "You must provide a target ID as the second parameter of AlsoToOneContent. If you want to send to all content processes, use BroadcastToContent" + ); + } + return _RouteMessage(action, { + from: MAIN_MESSAGE_TYPE, + to: CONTENT_MESSAGE_TYPE, + toTarget: target, + skipMain, + }); +} + +/** + * OnlyToOneContent - Creates a message that will be sent to a particular Content process + * and skip the main reducer. + * + * @param {object} action Any redux action (required) + * @param {string} target The id of a content port + * @return {object} An action with added .meta properties + */ +function OnlyToOneContent(action, target) { + return AlsoToOneContent(action, target, true); +} + +/** + * AlsoToPreloaded - Creates a message that dispatched to the main reducer and also sent to the preloaded tab. + * + * @param {object} action Any redux action (required) + * @return {object} An action with added .meta properties + */ +function AlsoToPreloaded(action) { + return _RouteMessage(action, { + from: MAIN_MESSAGE_TYPE, + to: PRELOAD_MESSAGE_TYPE, + }); +} + +/** + * UserEvent - A telemetry ping indicating a user action. This should only + * be sent from the UI during a user session. + * + * @param {object} data Fields to include in the ping (source, etc.) + * @return {object} An AlsoToMain action + */ +function UserEvent(data) { + return AlsoToMain({ + type: actionTypes.TELEMETRY_USER_EVENT, + data, + }); +} + +/** + * DiscoveryStreamUserEvent - A telemetry ping indicating a user action from Discovery Stream. This should only + * be sent from the UI during a user session. + * + * @param {object} data Fields to include in the ping (source, etc.) + * @return {object} An AlsoToMain action + */ +function DiscoveryStreamUserEvent(data) { + return AlsoToMain({ + type: actionTypes.DISCOVERY_STREAM_USER_EVENT, + data, + }); +} + +/** + * ASRouterUserEvent - A telemetry ping indicating a user action from AS router. This should only + * be sent from the UI during a user session. + * + * @param {object} data Fields to include in the ping (source, etc.) + * @return {object} An AlsoToMain action + */ +function ASRouterUserEvent(data) { + return AlsoToMain({ + type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT, + data, + }); +} + +/** + * ImpressionStats - A telemetry ping indicating an impression stats. + * + * @param {object} data Fields to include in the ping + * @param {int} importContext (For testing) Override the import context for testing. + * #return {object} An action. For UI code, a AlsoToMain action. + */ +function ImpressionStats(data, importContext = globalImportContext) { + const action = { + type: actionTypes.TELEMETRY_IMPRESSION_STATS, + data, + }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +/** + * DiscoveryStreamImpressionStats - A telemetry ping indicating an impression stats in Discovery Stream. + * + * @param {object} data Fields to include in the ping + * @param {int} importContext (For testing) Override the import context for testing. + * #return {object} An action. For UI code, a AlsoToMain action. + */ +function DiscoveryStreamImpressionStats( + data, + importContext = globalImportContext +) { + const action = { + type: actionTypes.DISCOVERY_STREAM_IMPRESSION_STATS, + data, + }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +/** + * DiscoveryStreamLoadedContent - A telemetry ping indicating a content gets loaded in Discovery Stream. + * + * @param {object} data Fields to include in the ping + * @param {int} importContext (For testing) Override the import context for testing. + * #return {object} An action. For UI code, a AlsoToMain action. + */ +function DiscoveryStreamLoadedContent( + data, + importContext = globalImportContext +) { + const action = { + type: actionTypes.DISCOVERY_STREAM_LOADED_CONTENT, + data, + }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +function SetPref(name, value, importContext = globalImportContext) { + const action = { type: actionTypes.SET_PREF, data: { name, value } }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +function WebExtEvent(type, data, importContext = globalImportContext) { + if (!data || !data.source) { + throw new Error( + 'WebExtEvent actions should include a property "source", the id of the webextension that should receive the event.' + ); + } + const action = { type, data }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +export const actionCreators = { + BroadcastToContent, + UserEvent, + DiscoveryStreamUserEvent, + ASRouterUserEvent, + ImpressionStats, + AlsoToOneContent, + OnlyToOneContent, + AlsoToMain, + OnlyToMain, + AlsoToPreloaded, + SetPref, + WebExtEvent, + DiscoveryStreamImpressionStats, + DiscoveryStreamLoadedContent, +}; + +// These are helpers to test for certain kinds of actions +export const actionUtils = { + isSendToMain(action) { + if (!action.meta) { + return false; + } + return ( + action.meta.to === MAIN_MESSAGE_TYPE && + action.meta.from === CONTENT_MESSAGE_TYPE + ); + }, + isBroadcastToContent(action) { + if (!action.meta) { + return false; + } + if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) { + return true; + } + return false; + }, + isSendToOneContent(action) { + if (!action.meta) { + return false; + } + if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) { + return true; + } + return false; + }, + isSendToPreloaded(action) { + if (!action.meta) { + return false; + } + return ( + action.meta.to === PRELOAD_MESSAGE_TYPE && + action.meta.from === MAIN_MESSAGE_TYPE + ); + }, + isFromMain(action) { + if (!action.meta) { + return false; + } + return ( + action.meta.from === MAIN_MESSAGE_TYPE && + action.meta.to === CONTENT_MESSAGE_TYPE + ); + }, + getPortIdOfSender(action) { + return (action.meta && action.meta.fromTarget) || null; + }, + _RouteMessage, +}; diff --git a/browser/components/newtab/common/ActorConstants.sys.mjs b/browser/components/newtab/common/ActorConstants.sys.mjs new file mode 100644 index 0000000000..c2874b53dd --- /dev/null +++ b/browser/components/newtab/common/ActorConstants.sys.mjs @@ -0,0 +1,47 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const MESSAGE_TYPE_LIST = [ + "BLOCK_MESSAGE_BY_ID", + "USER_ACTION", + "IMPRESSION", + "TRIGGER", + "NEWTAB_MESSAGE_REQUEST", + // PB is Private Browsing + "PBNEWTAB_MESSAGE_REQUEST", + "DOORHANGER_TELEMETRY", + "TOOLBAR_BADGE_TELEMETRY", + "TOOLBAR_PANEL_TELEMETRY", + "MOMENTS_PAGE_TELEMETRY", + "INFOBAR_TELEMETRY", + "SPOTLIGHT_TELEMETRY", + "TOAST_NOTIFICATION_TELEMETRY", + "AS_ROUTER_TELEMETRY_USER_EVENT", + + // Admin types + "ADMIN_CONNECT_STATE", + "UNBLOCK_MESSAGE_BY_ID", + "UNBLOCK_ALL", + "BLOCK_BUNDLE", + "UNBLOCK_BUNDLE", + "DISABLE_PROVIDER", + "ENABLE_PROVIDER", + "EVALUATE_JEXL_EXPRESSION", + "EXPIRE_QUERY_CACHE", + "FORCE_ATTRIBUTION", + "FORCE_WHATSNEW_PANEL", + "FORCE_PRIVATE_BROWSING_WINDOW", + "CLOSE_WHATSNEW_PANEL", + "OVERRIDE_MESSAGE", + "MODIFY_MESSAGE_JSON", + "RESET_PROVIDER_PREF", + "SET_PROVIDER_USER_PREF", + "RESET_GROUPS_STATE", +]; + +export const MESSAGE_TYPE_HASH = MESSAGE_TYPE_LIST.reduce((hash, value) => { + hash[value] = value; + return hash; +}, {}); diff --git a/browser/components/newtab/common/Dedupe.sys.mjs b/browser/components/newtab/common/Dedupe.sys.mjs new file mode 100644 index 0000000000..eedca8a0ee --- /dev/null +++ b/browser/components/newtab/common/Dedupe.sys.mjs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export class Dedupe { + constructor(createKey) { + this.createKey = createKey || this.defaultCreateKey; + } + + defaultCreateKey(item) { + return item; + } + + /** + * Dedupe any number of grouped elements favoring those from earlier groups. + * + * @param {Array} groups Contains an arbitrary number of arrays of elements. + * @returns {Array} A matching array of each provided group deduped. + */ + group(...groups) { + const globalKeys = new Set(); + const result = []; + for (const values of groups) { + const valueMap = new Map(); + for (const value of values) { + const key = this.createKey(value); + if (!globalKeys.has(key) && !valueMap.has(key)) { + valueMap.set(key, value); + } + } + result.push(valueMap); + valueMap.forEach((value, key) => globalKeys.add(key)); + } + return result.map(m => Array.from(m.values())); + } +} diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs new file mode 100644 index 0000000000..f0c0de4c82 --- /dev/null +++ b/browser/components/newtab/common/Reducers.sys.mjs @@ -0,0 +1,864 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs"; + +export const TOP_SITES_DEFAULT_ROWS = 1; +export const TOP_SITES_MAX_SITES_PER_ROW = 8; +const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible"; + +const dedupe = new Dedupe(site => site && site.url); + +export const INITIAL_STATE = { + App: { + // Have we received real data from the app yet? + initialized: false, + locale: "", + isForStartupCache: false, + customizeMenuVisible: false, + }, + ASRouter: { initialized: false }, + Snippets: { initialized: false }, + TopSites: { + // Have we received real data from history yet? + initialized: false, + // The history (and possibly default) links + rows: [], + // Used in content only to dispatch action to TopSiteForm. + editForm: null, + // Used in content only to open the SearchShortcutsForm modal. + showSearchShortcutsForm: false, + // The list of available search shortcuts. + searchShortcuts: [], + }, + Prefs: { + initialized: false, + values: { featureConfig: {} }, + }, + Dialog: { + visible: false, + data: {}, + }, + Sections: [], + Pocket: { + isUserLoggedIn: null, + pocketCta: {}, + waitingForSpoc: true, + }, + // This is the new pocket configurable layout state. + DiscoveryStream: { + // This is a JSON-parsed copy of the discoverystream.config pref value. + config: { enabled: false, layout_endpoint: "" }, + layout: [], + lastUpdated: null, + isPrivacyInfoModalVisible: false, + isCollectionDismissible: false, + feeds: { + data: { + // "https://foo.com/feed1": {lastUpdated: 123, data: []} + }, + loaded: false, + }, + spocs: { + spocs_endpoint: "", + lastUpdated: null, + data: { + // "spocs": {title: "", context: "", items: []}, + // "placement1": {title: "", context: "", items: []}, + }, + loaded: false, + frequency_caps: [], + blocked: [], + placements: [], + }, + experimentData: { + utmSource: "pocket-newtab", + utmCampaign: undefined, + utmContent: undefined, + }, + recentSavesData: [], + isUserLoggedIn: false, + recentSavesEnabled: false, + }, + Personalization: { + lastUpdated: null, + initialized: false, + }, + Search: { + // When search hand-off is enabled, we render a big button that is styled to + // look like a search textbox. If the button is clicked, we style + // the button as if it was a focused search box and show a fake cursor but + // really focus the awesomebar without the focus styles ("hidden focus"). + fakeFocus: false, + // Hide the search box after handing off to AwesomeBar and user starts typing. + hide: false, + }, +}; + +function App(prevState = INITIAL_STATE.App, action) { + switch (action.type) { + case at.INIT: + return Object.assign({}, prevState, action.data || {}, { + initialized: true, + }); + case at.TOP_SITES_UPDATED: + // Toggle `isForStartupCache` when receiving the `TOP_SITES_UPDATE` action + // so that sponsored tiles can be rendered as usual. See Bug 1826360. + return Object.assign({}, prevState, action.data || {}, { + isForStartupCache: false, + }); + case at.SHOW_PERSONALIZE: + return Object.assign({}, prevState, { + customizeMenuVisible: true, + }); + case at.HIDE_PERSONALIZE: + return Object.assign({}, prevState, { + customizeMenuVisible: false, + }); + default: + return prevState; + } +} + +function ASRouter(prevState = INITIAL_STATE.ASRouter, action) { + switch (action.type) { + case at.AS_ROUTER_INITIALIZED: + return { ...action.data, initialized: true }; + default: + return prevState; + } +} + +/** + * insertPinned - Inserts pinned links in their specified slots + * + * @param {array} a list of links + * @param {array} a list of pinned links + * @return {array} resulting list of links with pinned links inserted + */ +export function insertPinned(links, pinned) { + // Remove any pinned links + const pinnedUrls = pinned.map(link => link && link.url); + let newLinks = links.filter(link => + link ? !pinnedUrls.includes(link.url) : false + ); + newLinks = newLinks.map(link => { + if (link && link.isPinned) { + delete link.isPinned; + delete link.pinIndex; + } + return link; + }); + + // Then insert them in their specified location + pinned.forEach((val, index) => { + if (!val) { + return; + } + let link = Object.assign({}, val, { isPinned: true, pinIndex: index }); + if (index > newLinks.length) { + newLinks[index] = link; + } else { + newLinks.splice(index, 0, link); + } + }); + + return newLinks; +} + +function TopSites(prevState = INITIAL_STATE.TopSites, action) { + let hasMatch; + let newRows; + switch (action.type) { + case at.TOP_SITES_UPDATED: + if (!action.data || !action.data.links) { + return prevState; + } + return Object.assign( + {}, + prevState, + { initialized: true, rows: action.data.links }, + action.data.pref ? { pref: action.data.pref } : {} + ); + case at.TOP_SITES_PREFS_UPDATED: + return Object.assign({}, prevState, { pref: action.data.pref }); + case at.TOP_SITES_EDIT: + return Object.assign({}, prevState, { + editForm: { + index: action.data.index, + previewResponse: null, + }, + }); + case at.TOP_SITES_CANCEL_EDIT: + return Object.assign({}, prevState, { editForm: null }); + case at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL: + return Object.assign({}, prevState, { showSearchShortcutsForm: true }); + case at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL: + return Object.assign({}, prevState, { showSearchShortcutsForm: false }); + case at.PREVIEW_RESPONSE: + if ( + !prevState.editForm || + action.data.url !== prevState.editForm.previewUrl + ) { + return prevState; + } + return Object.assign({}, prevState, { + editForm: { + index: prevState.editForm.index, + previewResponse: action.data.preview, + previewUrl: action.data.url, + }, + }); + case at.PREVIEW_REQUEST: + if (!prevState.editForm) { + return prevState; + } + return Object.assign({}, prevState, { + editForm: { + index: prevState.editForm.index, + previewResponse: null, + previewUrl: action.data.url, + }, + }); + case at.PREVIEW_REQUEST_CANCEL: + if (!prevState.editForm) { + return prevState; + } + return Object.assign({}, prevState, { + editForm: { + index: prevState.editForm.index, + previewResponse: null, + }, + }); + case at.SCREENSHOT_UPDATED: + newRows = prevState.rows.map(row => { + if (row && row.url === action.data.url) { + hasMatch = true; + return Object.assign({}, row, { screenshot: action.data.screenshot }); + } + return row; + }); + return hasMatch + ? Object.assign({}, prevState, { rows: newRows }) + : prevState; + case at.PLACES_BOOKMARK_ADDED: + if (!action.data) { + return prevState; + } + newRows = prevState.rows.map(site => { + if (site && site.url === action.data.url) { + const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; + return Object.assign({}, site, { + bookmarkGuid, + bookmarkTitle, + bookmarkDateCreated: dateAdded, + }); + } + return site; + }); + return Object.assign({}, prevState, { rows: newRows }); + case at.PLACES_BOOKMARKS_REMOVED: + if (!action.data) { + return prevState; + } + newRows = prevState.rows.map(site => { + if (site && action.data.urls.includes(site.url)) { + const newSite = Object.assign({}, site); + delete newSite.bookmarkGuid; + delete newSite.bookmarkTitle; + delete newSite.bookmarkDateCreated; + return newSite; + } + return site; + }); + return Object.assign({}, prevState, { rows: newRows }); + case at.PLACES_LINKS_DELETED: + if (!action.data) { + return prevState; + } + newRows = prevState.rows.filter( + site => !action.data.urls.includes(site.url) + ); + return Object.assign({}, prevState, { rows: newRows }); + case at.UPDATE_SEARCH_SHORTCUTS: + return { ...prevState, searchShortcuts: action.data.searchShortcuts }; + case at.SNIPPETS_PREVIEW_MODE: + return { ...prevState, rows: [] }; + default: + return prevState; + } +} + +function Dialog(prevState = INITIAL_STATE.Dialog, action) { + switch (action.type) { + case at.DIALOG_OPEN: + return Object.assign({}, prevState, { visible: true, data: action.data }); + case at.DIALOG_CANCEL: + return Object.assign({}, prevState, { visible: false }); + case at.DELETE_HISTORY_URL: + return Object.assign({}, INITIAL_STATE.Dialog); + default: + return prevState; + } +} + +function Prefs(prevState = INITIAL_STATE.Prefs, action) { + let newValues; + switch (action.type) { + case at.PREFS_INITIAL_VALUES: + return Object.assign({}, prevState, { + initialized: true, + values: action.data, + }); + case at.PREF_CHANGED: + newValues = Object.assign({}, prevState.values); + newValues[action.data.name] = action.data.value; + return Object.assign({}, prevState, { values: newValues }); + default: + return prevState; + } +} + +function Sections(prevState = INITIAL_STATE.Sections, action) { + let hasMatch; + let newState; + switch (action.type) { + case at.SECTION_DEREGISTER: + return prevState.filter(section => section.id !== action.data); + case at.SECTION_REGISTER: + // If section exists in prevState, update it + newState = prevState.map(section => { + if (section && section.id === action.data.id) { + hasMatch = true; + return Object.assign({}, section, action.data); + } + return section; + }); + // Otherwise, append it + if (!hasMatch) { + const initialized = !!(action.data.rows && !!action.data.rows.length); + const section = Object.assign( + { title: "", rows: [], enabled: false }, + action.data, + { initialized } + ); + newState.push(section); + } + return newState; + case at.SECTION_UPDATE: + newState = prevState.map(section => { + if (section && section.id === action.data.id) { + // If the action is updating rows, we should consider initialized to be true. + // This can be overridden if initialized is defined in the action.data + const initialized = action.data.rows ? { initialized: true } : {}; + + // Make sure pinned cards stay at their current position when rows are updated. + // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards. + if ( + action.data.rows && + !!action.data.rows.length && + section.rows.find(card => card.pinned) + ) { + const rows = Array.from(action.data.rows); + section.rows.forEach((card, index) => { + if (card.pinned) { + // Only add it if it's not already there. + if (rows[index].guid !== card.guid) { + rows.splice(index, 0, card); + } + } + }); + return Object.assign( + {}, + section, + initialized, + Object.assign({}, action.data, { rows }) + ); + } + + return Object.assign({}, section, initialized, action.data); + } + return section; + }); + + if (!action.data.dedupeConfigurations) { + return newState; + } + + action.data.dedupeConfigurations.forEach(dedupeConf => { + newState = newState.map(section => { + if (section.id === dedupeConf.id) { + const dedupedRows = dedupeConf.dedupeFrom.reduce( + (rows, dedupeSectionId) => { + const dedupeSection = newState.find( + s => s.id === dedupeSectionId + ); + const [, newRows] = dedupe.group(dedupeSection.rows, rows); + return newRows; + }, + section.rows + ); + + return Object.assign({}, section, { rows: dedupedRows }); + } + + return section; + }); + }); + + return newState; + case at.SECTION_UPDATE_CARD: + return prevState.map(section => { + if (section && section.id === action.data.id && section.rows) { + const newRows = section.rows.map(card => { + if (card.url === action.data.url) { + return Object.assign({}, card, action.data.options); + } + return card; + }); + return Object.assign({}, section, { rows: newRows }); + } + return section; + }); + case at.PLACES_BOOKMARK_ADDED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.map(item => { + // find the item within the rows that is attempted to be bookmarked + if (item.url === action.data.url) { + const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; + return Object.assign({}, item, { + bookmarkGuid, + bookmarkTitle, + bookmarkDateCreated: dateAdded, + type: "bookmark", + }); + } + return item; + }), + }) + ); + case at.PLACES_SAVED_TO_POCKET: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.map(item => { + if (item.url === action.data.url) { + return Object.assign({}, item, { + open_url: action.data.open_url, + pocket_id: action.data.pocket_id, + title: action.data.title, + type: "pocket", + }); + } + return item; + }), + }) + ); + case at.PLACES_BOOKMARKS_REMOVED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.map(item => { + // find the bookmark within the rows that is attempted to be removed + if (action.data.urls.includes(item.url)) { + const newSite = Object.assign({}, item); + delete newSite.bookmarkGuid; + delete newSite.bookmarkTitle; + delete newSite.bookmarkDateCreated; + if (!newSite.type || newSite.type === "bookmark") { + newSite.type = "history"; + } + return newSite; + } + return item; + }), + }) + ); + case at.PLACES_LINKS_DELETED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.filter( + site => !action.data.urls.includes(site.url) + ), + }) + ); + case at.PLACES_LINK_BLOCKED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.filter(site => site.url !== action.data.url), + }) + ); + case at.DELETE_FROM_POCKET: + case at.ARCHIVE_FROM_POCKET: + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.filter( + site => site.pocket_id !== action.data.pocket_id + ), + }) + ); + case at.SNIPPETS_PREVIEW_MODE: + return prevState.map(section => ({ ...section, rows: [] })); + default: + return prevState; + } +} + +function Snippets(prevState = INITIAL_STATE.Snippets, action) { + switch (action.type) { + case at.SNIPPETS_DATA: + return Object.assign({}, prevState, { initialized: true }, action.data); + case at.SNIPPET_BLOCKED: + return Object.assign({}, prevState, { + blockList: prevState.blockList.concat(action.data), + }); + case at.SNIPPETS_BLOCKLIST_CLEARED: + return Object.assign({}, prevState, { blockList: [] }); + case at.SNIPPETS_RESET: + return INITIAL_STATE.Snippets; + default: + return prevState; + } +} + +function Pocket(prevState = INITIAL_STATE.Pocket, action) { + switch (action.type) { + case at.POCKET_WAITING_FOR_SPOC: + return { ...prevState, waitingForSpoc: action.data }; + case at.POCKET_LOGGED_IN: + return { ...prevState, isUserLoggedIn: !!action.data }; + case at.POCKET_CTA: + return { + ...prevState, + pocketCta: { + ctaButton: action.data.cta_button, + ctaText: action.data.cta_text, + ctaUrl: action.data.cta_url, + useCta: action.data.use_cta, + }, + }; + default: + return prevState; + } +} + +function Personalization(prevState = INITIAL_STATE.Personalization, action) { + switch (action.type) { + case at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED: + return { + ...prevState, + lastUpdated: action.data.lastUpdated, + }; + case at.DISCOVERY_STREAM_PERSONALIZATION_INIT: + return { + ...prevState, + initialized: true, + }; + default: + return prevState; + } +} + +// eslint-disable-next-line complexity +function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) { + // Return if action data is empty, or spocs or feeds data is not loaded + const isNotReady = () => + !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded; + + const handlePlacements = handleSites => { + const { data, placements } = prevState.spocs; + const result = {}; + + const forPlacement = placement => { + const placementSpocs = data[placement.name]; + + if ( + !placementSpocs || + !placementSpocs.items || + !placementSpocs.items.length + ) { + return; + } + + result[placement.name] = { + ...placementSpocs, + items: handleSites(placementSpocs.items), + }; + }; + + if (!placements || !placements.length) { + [{ name: "spocs" }].forEach(forPlacement); + } else { + placements.forEach(forPlacement); + } + return result; + }; + + const nextState = handleSites => ({ + ...prevState, + spocs: { + ...prevState.spocs, + data: handlePlacements(handleSites), + }, + feeds: { + ...prevState.feeds, + data: Object.keys(prevState.feeds.data).reduce( + (accumulator, feed_url) => { + accumulator[feed_url] = { + data: { + ...prevState.feeds.data[feed_url].data, + recommendations: handleSites( + prevState.feeds.data[feed_url].data.recommendations + ), + }, + }; + return accumulator; + }, + {} + ), + }, + }); + + switch (action.type) { + case at.DISCOVERY_STREAM_CONFIG_CHANGE: + // Fall through to a separate action is so it doesn't trigger a listener update on init + case at.DISCOVERY_STREAM_CONFIG_SETUP: + return { ...prevState, config: action.data || {} }; + case at.DISCOVERY_STREAM_EXPERIMENT_DATA: + return { ...prevState, experimentData: action.data || {} }; + case at.DISCOVERY_STREAM_LAYOUT_UPDATE: + return { + ...prevState, + lastUpdated: action.data.lastUpdated || null, + layout: action.data.layout || [], + }; + case at.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE: + return { + ...prevState, + isCollectionDismissible: action.data.value, + }; + case at.DISCOVERY_STREAM_PREFS_SETUP: + return { + ...prevState, + recentSavesEnabled: action.data.recentSavesEnabled, + pocketButtonEnabled: action.data.pocketButtonEnabled, + saveToPocketCard: action.data.saveToPocketCard, + hideDescriptions: action.data.hideDescriptions, + compactImages: action.data.compactImages, + imageGradient: action.data.imageGradient, + newSponsoredLabel: action.data.newSponsoredLabel, + titleLines: action.data.titleLines, + descLines: action.data.descLines, + readTime: action.data.readTime, + }; + case at.DISCOVERY_STREAM_RECENT_SAVES: + return { + ...prevState, + recentSavesData: action.data.recentSaves, + }; + case at.DISCOVERY_STREAM_POCKET_STATE_SET: + return { + ...prevState, + isUserLoggedIn: action.data.isUserLoggedIn, + }; + case at.HIDE_PRIVACY_INFO: + return { + ...prevState, + isPrivacyInfoModalVisible: false, + }; + case at.SHOW_PRIVACY_INFO: + return { + ...prevState, + isPrivacyInfoModalVisible: true, + }; + case at.DISCOVERY_STREAM_LAYOUT_RESET: + return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config }; + case at.DISCOVERY_STREAM_FEEDS_UPDATE: + return { + ...prevState, + feeds: { + ...prevState.feeds, + loaded: true, + }, + }; + case at.DISCOVERY_STREAM_FEED_UPDATE: + const newData = {}; + newData[action.data.url] = action.data.feed; + return { + ...prevState, + feeds: { + ...prevState.feeds, + data: { + ...prevState.feeds.data, + ...newData, + }, + }, + }; + case at.DISCOVERY_STREAM_SPOCS_CAPS: + return { + ...prevState, + spocs: { + ...prevState.spocs, + frequency_caps: [...prevState.spocs.frequency_caps, ...action.data], + }, + }; + case at.DISCOVERY_STREAM_SPOCS_ENDPOINT: + return { + ...prevState, + spocs: { + ...INITIAL_STATE.DiscoveryStream.spocs, + spocs_endpoint: + action.data.url || + INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint, + }, + }; + case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS: + return { + ...prevState, + spocs: { + ...prevState.spocs, + placements: + action.data.placements || + INITIAL_STATE.DiscoveryStream.spocs.placements, + }, + }; + case at.DISCOVERY_STREAM_SPOCS_UPDATE: + if (action.data) { + return { + ...prevState, + spocs: { + ...prevState.spocs, + lastUpdated: action.data.lastUpdated, + data: action.data.spocs, + loaded: true, + }, + }; + } + return prevState; + case at.DISCOVERY_STREAM_SPOC_BLOCKED: + return { + ...prevState, + spocs: { + ...prevState.spocs, + blocked: [...prevState.spocs.blocked, action.data.url], + }, + }; + case at.DISCOVERY_STREAM_LINK_BLOCKED: + return isNotReady() + ? prevState + : nextState(items => + items.filter(item => item.url !== action.data.url) + ); + + case at.PLACES_SAVED_TO_POCKET: + const addPocketInfo = item => { + if (item.url === action.data.url) { + return Object.assign({}, item, { + open_url: action.data.open_url, + pocket_id: action.data.pocket_id, + context_type: "pocket", + }); + } + return item; + }; + return isNotReady() + ? prevState + : nextState(items => items.map(addPocketInfo)); + + case at.DELETE_FROM_POCKET: + case at.ARCHIVE_FROM_POCKET: + return isNotReady() + ? prevState + : nextState(items => + items.filter(item => item.pocket_id !== action.data.pocket_id) + ); + + case at.PLACES_BOOKMARK_ADDED: + const updateBookmarkInfo = item => { + if (item.url === action.data.url) { + const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; + return Object.assign({}, item, { + bookmarkGuid, + bookmarkTitle, + bookmarkDateCreated: dateAdded, + context_type: "bookmark", + }); + } + return item; + }; + return isNotReady() + ? prevState + : nextState(items => items.map(updateBookmarkInfo)); + + case at.PLACES_BOOKMARKS_REMOVED: + const removeBookmarkInfo = item => { + if (action.data.urls.includes(item.url)) { + const newSite = Object.assign({}, item); + delete newSite.bookmarkGuid; + delete newSite.bookmarkTitle; + delete newSite.bookmarkDateCreated; + if (!newSite.context_type || newSite.context_type === "bookmark") { + newSite.context_type = "removedBookmark"; + } + return newSite; + } + return item; + }; + return isNotReady() + ? prevState + : nextState(items => items.map(removeBookmarkInfo)); + case at.PREF_CHANGED: + if (action.data.name === PREF_COLLECTION_DISMISSIBLE) { + return { + ...prevState, + isCollectionDismissible: action.data.value, + }; + } + return prevState; + default: + return prevState; + } +} + +function Search(prevState = INITIAL_STATE.Search, action) { + switch (action.type) { + case at.DISABLE_SEARCH: + return Object.assign({ ...prevState, disable: true }); + case at.FAKE_FOCUS_SEARCH: + return Object.assign({ ...prevState, fakeFocus: true }); + case at.SHOW_SEARCH: + return Object.assign({ ...prevState, disable: false, fakeFocus: false }); + default: + return prevState; + } +} + +export const reducers = { + TopSites, + App, + ASRouter, + Snippets, + Prefs, + Dialog, + Sections, + Pocket, + Personalization, + DiscoveryStream, + Search, +}; diff --git a/browser/components/newtab/components.conf b/browser/components/newtab/components.conf new file mode 100644 index 0000000000..25466d28e3 --- /dev/null +++ b/browser/components/newtab/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{dfcd2adc-7867-4d3a-ba70-17501f208142}', + 'contract_ids': ['@mozilla.org/browser/aboutnewtab-service;1'], + 'jsm': 'resource:///modules/AboutNewTabService.jsm', + 'constructor': 'AboutNewTabStubService', + }, +] diff --git a/browser/components/newtab/components/CustomElements/paragraph.js b/browser/components/newtab/components/CustomElements/paragraph.js new file mode 100644 index 0000000000..570183667e --- /dev/null +++ b/browser/components/newtab/components/CustomElements/paragraph.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { RemoteL10n } = ChromeUtils.importESModule( + "resource://activity-stream/lib/RemoteL10n.sys.mjs" + ); + class MozTextParagraph extends HTMLElement { + constructor() { + super(); + + this._content = null; + } + + get fluentAttributeValues() { + const attributes = {}; + for (let name of this.getAttributeNames()) { + if (name.startsWith("fluent-variable-")) { + let value = this.getAttribute(name); + // Attribute value is a string, in some cases that is not useful + // for example instantiating a Date object will fail. We try to + // convert all possible integers back. + if (value.match(/^\d+/)) { + value = parseInt(value, 10); + } + attributes[name.replace(/^fluent-variable-/, "")] = value; + } + } + + return attributes; + } + + render() { + if (this.getAttribute("fluent-remote-id") && this._content) { + RemoteL10n.l10n.setAttributes( + this._content, + this.getAttribute("fluent-remote-id"), + this.fluentAttributeValues + ); + } + } + + static get observedAttributes() { + return ["fluent-remote-id"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + this.render(); + } + + connectedCallback() { + if (this.shadowRoot) { + this.render(); + return; + } + + const shadowRoot = this.attachShadow({ mode: "open" }); + this._content = document.createElement("span"); + shadowRoot.appendChild(this._content); + + this.render(); + RemoteL10n.l10n.translateFragment(this._content); + } + } + + customElements.define("remote-text", MozTextParagraph); +} diff --git a/browser/components/newtab/content-src/.eslintrc.js b/browser/components/newtab/content-src/.eslintrc.js new file mode 100644 index 0000000000..bf7904cc43 --- /dev/null +++ b/browser/components/newtab/content-src/.eslintrc.js @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable import/no-commonjs */ + +module.exports = { + rules: { + "import/no-commonjs": 2, + "react/jsx-no-bind": 0, + }, +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx new file mode 100644 index 0000000000..18ef140618 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import ReactDOM from "react-dom"; +import { AboutWelcomeUtils } from "../lib/aboutwelcome-utils"; +import { MultiStageAboutWelcome } from "./components/MultiStageAboutWelcome"; +import { ReturnToAMO } from "./components/ReturnToAMO"; + +class AboutWelcome extends React.PureComponent { + constructor(props) { + super(props); + this.state = { metricsFlowUri: null }; + this.fetchFxAFlowUri = this.fetchFxAFlowUri.bind(this); + } + + async fetchFxAFlowUri() { + this.setState({ metricsFlowUri: await window.AWGetFxAMetricsFlowURI?.() }); + } + + componentDidMount() { + if (!this.props.skipFxA) { + this.fetchFxAFlowUri(); + } + // Record impression with performance data after allowing the page to load + const recordImpression = domState => { + const { domComplete, domInteractive } = performance + .getEntriesByType("navigation") + .pop(); + AboutWelcomeUtils.sendImpressionTelemetry(this.props.messageId, { + domComplete, + domInteractive, + mountStart: performance.getEntriesByName("mount").pop().startTime, + domState, + source: this.props.UTMTerm, + }); + }; + if (document.readyState === "complete") { + // Page might have already triggered a load event because it waited for async data, + // e.g., attribution, so the dom load timing could be of a empty content + // with domState in telemetry captured as 'complete' + recordImpression(document.readyState); + } else { + window.addEventListener("load", () => recordImpression("load"), { + once: true, + }); + } + + // Captures user has seen about:welcome by setting + // firstrun.didSeeAboutWelcome pref to true and capturing welcome UI unique messageId + window.AWSendToParent("SET_WELCOME_MESSAGE_SEEN", this.props.messageId); + } + + render() { + const { props } = this; + if (props.template === "return_to_amo") { + return ( + + ); + } + return ( + + ); + } +} + +// Computes messageId and UTMTerm info used in telemetry +function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) { + let messageId = + welcomeContent.template === "return_to_amo" + ? `RTAMO_DEFAULT_WELCOME_${welcomeContent.type.toUpperCase()}` + : "DEFAULT_ID"; + let UTMTerm = "aboutwelcome-default"; + + if (welcomeContent.id) { + messageId = welcomeContent.id.toUpperCase(); + } + + if (experimentId && branchId) { + UTMTerm = `aboutwelcome-${experimentId}-${branchId}`.toLowerCase(); + } + return { + messageId, + UTMTerm, + }; +} + +async function retrieveRenderContent() { + // Feature config includes RTAMO attribution data if exists + // else below data in order specified + // user prefs + // experiment data + // defaults + let featureConfig = await window.AWGetFeatureConfig(); + + let { messageId, UTMTerm } = ComputeTelemetryInfo( + featureConfig, + featureConfig.slug, + featureConfig.branch && featureConfig.branch.slug + ); + return { featureConfig, messageId, UTMTerm }; +} + +async function mount() { + let { + featureConfig: aboutWelcomeProps, + messageId, + UTMTerm, + } = await retrieveRenderContent(); + ReactDOM.render( + , + document.getElementById("multi-stage-message-root") + ); +} + +performance.mark("mount"); +mount(); diff --git a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss new file mode 100644 index 0000000000..f4f756dfd0 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss @@ -0,0 +1,1676 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@use 'sass:math'; +@import '../styles/feature-callout'; + +$break-point-small: 570px; +$break-point-medium: 610px; +$break-point-large: 866px; +$container-min-width: 700px; +$logo-size: 80px; +$main-section-width: 504px; +$split-section-width: 400px; +$split-screen-height: 550px; +$small-main-section-height: 450px; +$small-secondary-section-height: 100px; +$noodle-buffer: 106px; +$video-section-width: 800px; + +html { + height: 100%; +} + +// Below variables are used via config JSON in AboutWelcomeDefaults +// and referenced below inside dummy class to pass test browser_parsable_css +.dummy { + background: var(--mr-welcome-background-color) var(--mr-welcome-background-gradient) var(--mr-secondary-position) var(--mr-screen-background-color); +} + +// Styling for content rendered in a Spotlight messaging surface. +:root { + &[dialogroot] { + background-color: transparent; + + body { + padding: 0; + } + + .onboardingContainer { + // Without this, the container will be 100vh in height. When the dialog + // overflows horizontally, the horizontal scrollbar will appear. If the + // scrollbars aren't overlay scrollbars (this is controlled by + // Theme::ScrollbarStyle), they will take up vertical space in the + // viewport, causing the dialog to overflow vertically. This causes the + // vertical scrollbar to appear, which takes up horizontal space, causing + // the horizontal scrollbar to appear, and so on. + height: 100%; + background-color: transparent; + + &:dir(rtl) { + transform: unset; + } + + .logo-container { + pointer-events: none; + } + + .screen { + &:dir(rtl) { + transform: unset; + } + } + } + } +} + +// Styling for about:welcome background container +.welcome-container { + .onboardingContainer { + min-height: $break-point-medium; + min-width: fit-content; + } +} + +.onboardingContainer { + --grey-subtitle-1: #696977; + --mr-welcome-background-color: #F8F6F4; + --mr-screen-heading-color: var(--in-content-text-color); + --mr-welcome-background-gradient: linear-gradient(0deg, rgba(144, 89, 255, 20%) 0%, rgba(2, 144, 238, 20%) 100%); + --mr-screen-background-color: #F8F6F4; + + @media (prefers-color-scheme: dark) { + --grey-subtitle-1: #FFF; + --mr-welcome-background-color: #333336; + --mr-welcome-background-gradient: linear-gradient(0deg, rgba(144, 89, 255, 30%) 0%, rgba(2, 144, 238, 30%) 100%); + --mr-screen-background-color: #62697A; + } + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Ubuntu, + 'Helvetica Neue', sans-serif; + font-size: 16px; + position: relative; + text-align: center; + height: 100vh; + + @media (prefers-contrast) { + --mr-screen-background-color: buttontext; + --mr-screen-heading-color: buttonface; + + background-color: var(--in-content-page-background); + } + + // Transition all of these and reduced motion effectively only does opacity. + --transition: 0.6s opacity, 0.6s scale, 0.6s rotate, 0.6s translate; + + // Define some variables that are used for in/out states. + @media (prefers-reduced-motion: no-preference) { + --translate: 30px; + --rotate: 20deg; + --scale: 0.4; + --progress-bar-transition: 0.6s translate; + + // Scale is used for noodles that can be flipped. + &:dir(rtl) { + --scale: -0.4 0.4; + } + } + + // Use default values that match "unmoved" state to avoid motion. + @media (prefers-reduced-motion: reduce) { + --translate: 0; + --rotate: 0deg; + --scale: 1; + // To reduce motion, progress bar fades in instead of wiping in. + --progress-bar-transition: none; + + &:dir(rtl) { + --scale: -1 1; + } + } + + &:dir(rtl) { + transform: rotateY(180deg); + } + + .section-main { + display: flex; + flex-direction: column; + justify-content: center; + width: $main-section-width; + flex-shrink: 0; + } + + .section-main:not(.embedded-migration) { + position: relative; + } + + .main-content { + background-color: var(--in-content-page-background); + border-radius: 20px; + box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 20%); + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + transition: var(--transition); + z-index: 1; + box-sizing: border-box; + + &.no-steps { + padding-bottom: 48px; + } + + .main-content-inner { + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: space-around; + } + } + + // Handle conditional display of steps indicator + // Don't show when there's only one screen + .main-content .no-steps { + .main-content { + padding-bottom: 48px; + } + + .steps { + display: none; + } + } + + @mixin arrow-icon-styles { + .arrow-icon { + -moz-context-properties: fill; + fill: currentColor; + text-decoration: none; + + &::after { + content: ''; + padding-inline-end: 12px; + margin-inline-start: 4px; + background: url('chrome://browser/skin/forward.svg') no-repeat center / 12px; + } + + &:dir(rtl)::after { + background-image: url('chrome://browser/skin/back.svg'); + } + } + } + + @mixin secondary-cta-styles { + background-color: var(--in-content-button-background) !important; // stylelint-disable-line declaration-no-important + border: 1px solid var(--in-content-button-border-color); + line-height: 12px; + font-size: 0.72em; + font-weight: 600; + padding: 8px 16px; + text-decoration: none; + + &:hover { + background-color: var(--in-content-button-background-hover) !important; // stylelint-disable-line declaration-no-important + color: var(--in-content-button-text-color-hover); + } + } + + @mixin text-link-styles { + text-decoration: underline; + cursor: pointer; + color: var(--in-content-link-color); + + &:hover { + text-decoration: none; + color: var(--in-content-link-color-hover); + } + + &:active { + text-decoration: none; + color: var(--in-content-link-color-active); + } + } + + .screen { + display: flex; + position: relative; + flex-flow: row nowrap; + height: 100%; + min-height: 500px; + overflow: hidden; + + --in-content-link-color: var(--in-content-primary-button-background); + --in-content-link-color-hover: var(--in-content-primary-button-background-hover); + --in-content-link-color-active: var(--in-content-primary-button-background-active); + --in-content-link-color-visited: var(--in-content-link-color); + + &.light-text { + --in-content-page-color: rgb(251, 251, 254); + --in-content-primary-button-text-color: rgb(43, 42, 51); + --in-content-primary-button-text-color-hover: rgb(43, 42, 51); + --in-content-primary-button-background: rgb(0, 221, 255); + --in-content-primary-button-background-hover: rgb(128, 235, 255); + --in-content-primary-button-background-active: rgb(170, 242, 255); + --checkbox-checked-bgcolor: var(--in-content-primary-button-background); + --in-content-button-text-color: var(--in-content-page-color); + } + + &.dark-text { + --in-content-page-color: rgb(21, 20, 26); + --in-content-primary-button-text-color: rgb(251, 251, 254); + --in-content-primary-button-text-color-hover: rgb(251, 251, 254); + --in-content-primary-button-background: #0061E0; + --in-content-primary-button-background-hover: #0250BB; + --in-content-primary-button-background-active: #053E94; + --in-content-primary-button-border-color: transparent; + --in-content-primary-button-border-hover: transparent; + --checkbox-checked-bgcolor: var(--in-content-primary-button-background); + --in-content-button-text-color: var(--in-content-page-color); + } + + &:dir(rtl) { + transform: rotateY(180deg); + } + + &[pos='center'] { + background-color: rgba(21, 20, 26, 50%); + min-width: $main-section-width; + + &.with-noodles { + // Adjust for noodles partially extending out from the square modal + min-width: $main-section-width + $noodle-buffer; + min-height: $main-section-width + $noodle-buffer; + + .section-main { + height: $main-section-width; + } + } + + &.with-video { + justify-content: center; + background: none; + align-items: center; + + .section-main { + width: $video-section-width; + height: $split-screen-height; + } + + .main-content { + background-color: var(--mr-welcome-background-color); + border-radius: 8px; + box-shadow: 0 2px 14px rgba(58, 57, 68, 20%); + padding: 44px 85px 20px; + + .welcome-text { + margin: 0; + } + + .main-content-inner { + justify-content: space-between; + } + + h1, + h2 { + align-self: start; + } + + h1 { + font-size: 24px; + line-height: 28.8px; + } + + h2 { + font-size: 15px; + line-height: 22px; + } + + .secondary-cta { + @include arrow-icon-styles; + + justify-content: end; + + .secondary { + @include secondary-cta-styles; + + color: var(--in-content-button-text-color); + } + } + } + } + } + + &:not([pos='split']) { + .secondary-cta { + .secondary { + background: none; + color: var(--in-content-link-color); + font-size: 14px; + font-weight: normal; + line-height: 20px; + } + + &.top { + button { + color: #FFF; + + &:hover { + color: #E0E0E6; + } + } + } + } + } + + &[pos='split'] { + margin: auto; + min-height: $split-screen-height; + + &::before { + content: ''; + position: absolute; + box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 20%); + width: $split-section-width + $split-section-width; + height: $split-screen-height; + border-radius: 8px; + inset: 0; + margin: auto; + pointer-events: none; + } + + .section-secondary, + .section-main { + width: $split-section-width; + height: $split-screen-height; + } + + .secondary-cta.top { + position: fixed; + padding-inline-end: 0; + + button { + color: var(--in-content-page-color); + } + } + + .section-main { + flex-direction: row; + display: block; + margin: auto auto auto 0; + + &:dir(rtl) { + margin: auto 0 auto auto; + } + + .main-content { + border-radius: 0 8px 8px 0; + overflow: hidden; + padding-inline: 35px 20px; + padding-block: 120px 0; + box-shadow: none; + + &.no-steps { + padding-bottom: 48px; + } + + &:dir(rtl) { + border-radius: 8px 0 0 8px; + } + + .main-content-inner { + min-height: 330px; + + .language-switcher-container { + .primary { + margin-bottom: 5px; + } + } + } + + .action-buttons { + position: relative; + text-align: initial; + height: 100%; + + .checkbox-container { + font-size: 13px; + margin-block: 1em; + + &:not(.multi-select-item) { + transition: var(--transition); + } + + input, + label { + vertical-align: middle; + } + } + + .additional-cta { + margin: 8px 0; + + &.cta-link { + background: none; + padding: 0; + font-weight: normal; + + @include text-link-styles; + } + + &.secondary { + &:hover { + background-color: var(--in-content-button-background-hover); + } + } + } + + &.additional-cta-container { + flex-wrap: nowrap; + align-items: start; + } + + .secondary-cta { + position: absolute; + bottom: -30px; + inset-inline-end: 0; + + .secondary { + @include secondary-cta-styles; + } + + @include arrow-icon-styles; + } + } + + .logo-container { + text-align: start; + } + + .brand-logo { + height: 25px; + margin-block: 0; + } + + .welcome-text { + margin-inline: 0 10px; + margin-block: 10px 35px; + text-align: initial; + align-items: initial; + + &:empty { + margin: 0; + } + + h1 { + font-size: 24px; + line-height: 1.2; + width: 300px; + } + + h2 { + margin: 10px 0 0; + min-height: 1em; + font-size: 15px; + line-height: 1.5; + + @media (prefers-contrast: no-preference) { + color: #5B5B66; + } + } + } + + .welcome-text h1, + .primary { + margin: 0; + } + + .steps { + z-index: 1; + + &.progress-bar { + width: $split-section-width; + margin-inline: -35px; + } + } + + @media (prefers-contrast) { + border: 1px solid var(--in-content-page-color); + + .steps.progress-bar { + border-top: 1px solid var(--in-content-page-color); + background-color: var(--in-content-page-background); + + .indicator { + background-color: var(--in-content-accent-color); + } + } + } + } + } + + .section-secondary { + --mr-secondary-position: center center / auto 350px; + + border-radius: 8px 0 0 8px; + margin: auto 0 auto auto; + display: flex; + align-items: center; + -moz-context-properties: fill, stroke, fill-opacity, stroke-opacity; + stroke: currentColor; + + &:dir(rtl) { + border-radius: 0 8px 8px 0; + margin: auto auto auto 0; + } + + h1 { + color: var(--mr-screen-heading-color); + font-weight: 700; + font-size: 47px; + line-height: 110%; + max-width: 340px; + text-align: initial; + white-space: pre-wrap; + text-shadow: none; + margin-inline: 40px 0; + } + + .image-alt { + width: inherit; + height: inherit; + } + + .hero-image { + flex: 1; + display: flex; + justify-content: center; + max-height: 100%; + + img { + width: 100%; + max-width: 180px; + margin: 25px 0; + padding-bottom: 30px; + + @media only screen and (max-width: 800px) { + padding-bottom: unset; + } + } + } + } + + .tiles-theme-container { + margin-block: -20px auto; + align-items: initial; + + .colorway-text { + text-align: initial; + transition: var(--transition); + font-size: 13px; + line-height: 1.5; + min-height: 4.5em; + margin-block: 10px 20px; + } + + .theme { + min-width: 38px; + } + } + + @media (prefers-contrast: no-preference) and (prefers-color-scheme: dark) { + .section-main .main-content { + .welcome-text h2 { + color: #CFCFD8; + } + + .action-buttons .secondary { + background-color: #2B2A33; + } + } + } + + @media only screen and (min-width: 800px) { + .tiles-theme-section { + margin-inline-start: -10px; + } + } + + @media only screen and (max-width: 800px) { + flex-direction: column; + min-height: $small-main-section-height + $small-secondary-section-height; + + &::before { + width: $split-section-width; + } + + .section-secondary, + .section-main { + width: $split-section-width; + } + + .section-secondary { + --mr-secondary-background-position-y: top; + --mr-secondary-position: center var(--mr-secondary-background-position-y) / 75%; + + border-radius: 8px 8px 0 0; + margin: auto auto 0; + height: $small-secondary-section-height; + + .hero-image img { + margin: 6px 0; + } + + .message-text { + margin-inline: auto; + } + + h1 { + font-size: 35px; + text-align: center; + white-space: normal; + margin-inline: auto; + margin-block: 14px 6px; + } + + &:dir(rtl) { + margin: auto auto 0; + border-radius: 8px 8px 0 0; + } + } + + .section-main { + margin: 0 auto auto; + height: $small-main-section-height; + + .main-content { + border-radius: 0 0 8px 8px; + padding: 30px 0 0; + + .main-content-inner { + align-items: center; + } + + .logo-container { + text-align: center; + + .brand-logo { + min-height: 25px; + + &, + &:dir(rtl) { + background-position: center; + } + } + } + + .welcome-text { + align-items: center; + text-align: center; + margin-inline: 0; + padding-inline: 30px; + + .spacer-bottom, + .spacer-top { + display: none; + } + } + + .action-buttons { + text-align: center; + + .checkbox-container { + display: none; + } + + .secondary-cta { + position: relative; + margin-block: 10px 0; + bottom: 0; + } + } + + .primary, + .secondary { + min-width: 240px; + } + + .colorway-text { + text-align: center; + margin-inline: 30px; + } + + .steps { + padding-block: 0; + margin: 0; + + &.progress-bar { + margin-inline: 0; + } + } + } + + .additional-cta { + &.cta-link { + align-self: center; + } + } + + .dismiss-button { + top: -$small-secondary-section-height; + } + + &:dir(rtl) { + margin: 0 auto auto; + + .main-content { + border-radius: 0 0 8px 8px; + } + } + } + + } + + @media only screen and (max-height: 650px) { + // Hide the "Sign in" button on the welcome screen when it would + // otherwise overlap the screen. We'd reposition it, but then it would + // overlap the dismiss button. We may change the alignment so they don't + // overlap in a future revision. + @media (min-width: 800px) and (max-width: 990px) { + .section-main .secondary-cta.top { + display: none; + } + } + + // Reposition the "Sign in" button on the welcome screen to move inside + // the screen when it would otherwise overlap the screen. + @media (max-width: 590px) { + .section-main .secondary-cta.top { + position: absolute; + padding: 0; + top: 0; + inset-inline-end: 0; + } + } + } + } + } + + .brand-logo { + margin-block: 60px 10px; + transition: var(--transition); + height: 80px; + + &.cta-top { + margin-top: 25px; + } + + &.hide { + visibility: hidden; + padding: unset; + margin-top: 50px; + } + } + + .rtamo-theme-icon { + max-height: 30px; + border-radius: 2px; + margin-bottom: 10px; + margin-top: 24px; + } + + .rtamo-icon { + text-align: start; + + @media only screen and (max-width: 800px) { + text-align: center; + } + } + + .text-link { + @include text-link-styles; + } + + .welcome-text { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 0.5em 1em; + transition: var(--transition); + + h1, + h2 { + color: var(--in-content-page-color); + line-height: 1.5; + } + + h1 { + font-size: 24px; + font-weight: 600; + margin: 0 6px; + letter-spacing: -0.02em; + outline: none; + } + + h2 { + font-size: 16px; + font-weight: normal; + margin: 10px 6px 0; + max-width: 750px; + letter-spacing: -0.01em; + } + + &.fancy { + h1 { + background-image: linear-gradient(90deg, #9059FF, #FF4AA2, #FF8C00, #FF4AA2, #9059FF); + background-clip: text; + background-size: 200%; + + @media (prefers-contrast: no-preference) { + color: transparent; + } + + @media (prefers-color-scheme: dark) { + background-image: linear-gradient(90deg, #C688FF, #FF84C0, #FFBD4F, #FF84C0, #C688FF); + + &::selection { + color: #FFF; + background-color: #696977; + } + } + } + } + + &.shine { + h1 { + animation: shine 50s linear infinite; + background-size: 400%; + } + + @keyframes shine { + to { + background-position: 400%; + } + } + } + + .cta-paragraph { + a { + margin: 0; + text-decoration: underline; + cursor: pointer; + } + } + } + + // Override light and dark mode fancy title colors for use over light and dark backgrounds + .screen.light-text .welcome-text.fancy h1 { + background-image: linear-gradient(90deg, #C688FF, #FF84C0, #FFBD4F, #FF84C0, #C688FF); + } + + .screen.dark-text .welcome-text.fancy h1 { + background-image: linear-gradient(90deg, #9059FF, #FF4AA2, #FF8C00, #FF4AA2, #9059FF); + } + + .welcomeZap { + span { + position: relative; + z-index: 1; + white-space: nowrap; + } + + .zap { + &::after { + display: block; + background-repeat: no-repeat; + background-size: 100% 100%; + content: ''; + position: absolute; + top: calc(100% - 0.15em); + width: 100%; + height: 0.3em; + left: 0; + z-index: -1; + transform: scaleY(3); + } + + &.short::after { + background-image: url('chrome://activity-stream/content/data/content/assets/short-zap.svg'); + } + + &.long::after { + background-image: url('chrome://activity-stream/content/data/content/assets/long-zap.svg'); + } + } + } + + .language-loader { + filter: invert(1); + margin-inline-end: 10px; + position: relative; + top: 3px; + width: 16px; + height: 16px; + margin-top: -6px; + } + + @media (prefers-color-scheme: dark) { + .language-loader { + filter: invert(0); + } + } + + .tiles-theme-container { + display: flex; + flex-direction: column; + align-items: center; + margin: 10px auto; + } + + .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + + &.input { + height: 1px; + width: 1px; + } + } + + .tiles-theme-section { + border: 0; + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: space-evenly; + margin-inline: 10px; + padding: 10px; + transition: var(--transition); + + &:hover, + &:active, + &:focus-within { + border-radius: 8px; + outline: 2px solid var(--in-content-primary-button-background); + } + + .theme { + align-items: center; + display: flex; + flex-direction: column; + flex: 1; + padding: 0; + min-width: 50px; + width: 180px; + color: #000; + box-shadow: none; + border-radius: 4px; + cursor: pointer; + z-index: 0; + + &.colorway { + width: auto; + } + + &:focus, + &:active { + outline: initial; + outline-offset: initial; + } + + .icon.colorway, + .label.colorway { + width: 20px; + height: 20px; + } + + .icon { + background-size: cover; + width: 40px; + height: 40px; + border-radius: 40px; + outline: 1px solid var(--in-content-border-color); + outline-offset: -0.5px; + z-index: -1; + + &:dir(rtl) { + transform: scaleX(-1); + } + + &:focus, + &:active, + &.selected { + outline: 2px solid var(--in-content-primary-button-background); + outline-offset: 2px; + } + + &.light { + background-image: url('resource://builtin-themes/light/icon.svg'); + } + + &.dark { + background-image: url('resource://builtin-themes/dark/icon.svg'); + } + + &.alpenglow { + background-image: url('resource://builtin-themes/alpenglow/icon.svg'); + } + + &.default, + &.automatic { + background-image: url('resource://default-theme/icon.svg'); + + &.colorway { + background-image: url('chrome://activity-stream/content/data/content/assets/default.svg'); + } + } + + &.playmaker { + background-image: url('resource://builtin-themes/colorways/2022playmaker/balanced/icon.svg'); + } + + &.expressionist { + background-image: url('resource://builtin-themes/colorways/2022expressionist/balanced/icon.svg'); + } + + &.visionary { + background-image: url('resource://builtin-themes/colorways/2022visionary/balanced/icon.svg'); + } + + &.dreamer { + background-image: url('resource://builtin-themes/colorways/2022dreamer/balanced/icon.svg'); + } + + &.innovator { + background-image: url('resource://builtin-themes/colorways/2022innovator/balanced/icon.svg'); + } + + &.activist { + background-image: url('resource://builtin-themes/colorways/2022activist/balanced/icon.svg'); + } + } + + .text { + display: flex; + color: var(--in-content-page-color); + font-size: 14px; + font-weight: normal; + line-height: 20px; + margin-inline-start: 0; + margin-top: 9px; + } + } + + legend { + cursor: default; + } + } + + .tiles-container { + margin: 10px auto; + + &.info { + padding: 6px 12px 12px; + + &:hover, + &:focus { + background-color: rgba(217, 217, 227, 30%); + border-radius: 4px; + } + } + } + + .tiles-delayed { + animation: fadein 0.4s; + } + + .multi-select-container { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-block: -1em 1em; + color: #5B5B66; + font-weight: 400; + font-size: 14px; + text-align: initial; + transition: var(--transition); + z-index: 1; + + .checkbox-container { + display: flex; + margin-bottom: 16px; + } + } + + @media (prefers-color-scheme: dark) { + .multi-select-container { + color: #CFCFD8; + } + } + + @media only screen and (max-width: 800px) { + .multi-select-container { + padding: 0 30px; + } + } + + .mobile-downloads { + .qr-code-image { + margin: 24px 0 10px; + width: 113px; + height: 113px; + } + + .email-link { + font-size: 16px; + font-weight: 400; + background: none; + + @include text-link-styles; + + &:hover { + background: none; + } + } + + .ios button { + background-image: url('chrome://app-marketplace-icons/locale/ios.svg'); + } + + .android button { + background-image: url('chrome://app-marketplace-icons/locale/android.png'); + } + } + + .mobile-download-buttons { + list-style: none; + padding: 10px 0; + margin: 0; + + li { + display: inline-block; + + button { + display: inline-block; + height: 45px; + width: 152px; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + box-shadow: none; + border: 0; + } + + &:not(:first-child) { + margin-inline: 5px 0; + } + } + } + + .dismiss-button { + position: absolute; + z-index: 1; + top: 0; + left: auto; + right: 0; + box-sizing: border-box; + padding: 0; + margin: 16px; + display: block; + float: inline-end; + background: url('chrome://global/skin/icons/close.svg') no-repeat center / 16px; + height: 32px; + width: 32px; + align-self: end; + // override default min-height and min-width for buttons + min-height: 32px; + min-width: 32px; + -moz-context-properties: fill; + fill: currentColor; + transition: var(--transition); + + &:dir(rtl) { + left: 0; + right: auto; + } + } + + @keyframes fadein { + from { opacity: 0; } + } + + .secondary-cta { + display: flex; + align-items: end; + flex-direction: row; + justify-content: center; + font-size: 14px; + transition: var(--transition); + + &.top { + justify-content: end; + padding-inline-end: min(150px, 500px - 70vh); + padding-top: 4px; + position: absolute; + top: 10px; + inset-inline-end: 20px; + z-index: 2; + } + + span { + color: var(--grey-subtitle-1); + margin: 0 4px; + } + } + + .message-text, + .attrib-text { + transition: var(--transition); + } + + .helptext { + padding: 1em; + text-align: center; + color: var(--grey-subtitle-1); + font-size: 12px; + line-height: 18px; + + &.default { + align-self: center; + max-width: 40%; + } + + span { + padding-inline-end: 4px; + } + } + + .helptext-img { + height: 1.5em; + width: 1.5em; + margin-inline-end: 4px; + vertical-align: middle; + + &.end { + margin: 4px; + } + + &.footer { + vertical-align: bottom; + } + } + + .steps { + display: flex; + flex-direction: row; + justify-content: center; + margin-top: 0; + padding-block: 16px 0; + transition: var(--transition); + z-index: -1; + height: 48px; + box-sizing: border-box; + + &.has-helptext { + padding-bottom: 0; + } + + .indicator { + width: 0; + height: 0; + margin-inline-end: 4px; + margin-inline-start: 4px; + background: var(--grey-subtitle-1); + border-radius: 5px; + // using border will show up in Windows High Contrast Mode to improve accessibility. + border: 3px solid var(--in-content-button-text-color); + opacity: 0.35; + box-sizing: inherit; + + &.current { + opacity: 1; + border-color: var(--checkbox-checked-bgcolor); + + // This is the only step shown, so visually hide it to maintain spacing. + &:last-of-type:first-of-type { + opacity: 0; + } + } + } + + &.progress-bar { + height: 6px; + padding-block: 0; + margin-block: 42px 0; + background-color: color-mix(in srgb, var(--in-content-button-text-color) 25%, transparent); + justify-content: start; + opacity: 1; + transition: none; + + .indicator { + width: 100%; + height: 100%; + margin-inline: -1px; + background-color: var(--checkbox-checked-bgcolor); + border: 0; + border-radius: 0; + opacity: 1; + transition: var(--progress-bar-transition); + translate: calc(var(--progress-bar-progress, 0%) - 100%); + + &:dir(rtl) { + translate: calc(var(--progress-bar-progress, 0%) * -1 + 100%); + } + } + } + } + + .additional-cta-container { + &[flow] { + display: flex; + flex-flow: column wrap; + align-items: center; + + &[flow='row'] { + flex-direction: row; + justify-content: center; + + .secondary-cta { + flex-basis: 100%; + } + } + } + } + + .primary, + .secondary, + .additional-cta { + font-size: 13px; + line-height: 16px; + padding: 11px 15px; + transition: var(--transition); + + &.rtamo { + margin-top: 24px; + } + } + + .secondary { + background-color: var(--in-content-button-background); + color: var(--in-content-button-text-color); + } + + // Styles specific to background noodles, with screen-by-screen positions + .noodle { + display: block; + background-repeat: no-repeat; + background-size: 100% 100%; + position: absolute; + transition: var(--transition); + + // Flip noodles in a way that combines individual transforms. + &:dir(rtl) { + scale: -1 1; + } + } + + .outline-L { + background-image: url('chrome://activity-stream/content/data/content/assets/noodle-outline-L.svg'); + } + + .solid-L { + background-image: url('chrome://activity-stream/content/data/content/assets/noodle-solid-L.svg'); + -moz-context-properties: fill; + fill: var(--in-content-page-background); + display: none; + } + + .purple-C { + background-image: url('chrome://activity-stream/content/data/content/assets/noodle-C.svg'); + -moz-context-properties: fill; + fill: #E7258C; + } + + .orange-L { + background-image: url('chrome://activity-stream/content/data/content/assets/noodle-solid-L.svg'); + -moz-context-properties: fill; + fill: #FFA437; + } + + .screen-1 { + .section-main { + z-index: 1; + margin: auto; + } + + // Position of noodles on second screen + .outline-L { + width: 87px; + height: 80px; + transform: rotate(10deg) translate(-30%, 200%); + transition-delay: 0.4s; + z-index: 2; + } + + .orange-L { + width: 550px; + height: 660px; + transform: rotate(-155deg) translate(11%, -18%); + transition-delay: 0.2s; + } + + .purple-C { + width: 310px; + height: 260px; + transform: translate(-18%, -67%); + } + + .yellow-circle { + width: 165px; + height: 165px; + border-radius: 50%; + transform: translate(230%, -5%); + background: #952BB9; + transition-delay: -0.2s; + } + } + + // Defining the timing of the transition-in for items within the dialog, + // These longer delays are to allow for the dialog to slide down on first screen + .dialog-initial { + .brand-logo { + transition-delay: 0.6s; + } + + .welcome-text { + transition-delay: 0.8s; + } + + .tiles-theme-section, + .multi-select-container, + migration-wizard { + transition-delay: 0.9s; + } + + .primary, + .secondary, + .secondary-cta, + .steps, + .cta-link { + transition-delay: 1s; + } + } + + // Delays for transitioning-in of intermediate screens + .screen:not(.dialog-initial) { + .tiles-theme-section, + .multi-select-container, + .colorway-text { + transition-delay: 0.2s; + } + + .primary, + .secondary, + .secondary-cta, + .cta-link { + transition-delay: 0.4s; + } + } + + .screen-2 { + .section-main { + z-index: 1; + margin: auto; + } + + // Position of noodles on third screen + .outline-L { + width: 87px; + height: 80px; + transform: rotate(250deg) translate(-420%, 425%); + transition-delay: 0.2s; + z-index: 2; + } + + .orange-L { + height: 800px; + width: 660px; + transform: rotate(35deg) translate(-10%, -7%); + transition-delay: -0.4s; + } + + .purple-C { + width: 392px; + height: 394px; + transform: rotate(260deg) translate(-34%, -35%); + transition-delay: -0.2s; + fill: #952BB9; + } + + .yellow-circle { + width: 165px; + height: 165px; + border-radius: 50%; + transform: translate(160%, 130%); + background: #E7258C; + } + } + + &.transition-in { + .noodle { + opacity: 0; + rotate: var(--rotate); + scale: var(--scale); + } + + .dialog-initial { + .main-content, + .dismiss-button { + translate: 0 calc(-2 * var(--translate)); + } + + .brand-logo, + .steps { + opacity: 0; + translate: 0 calc(-1 * var(--translate)); + } + } + + .screen { + .welcome-text, + .multi-select-container, + .tiles-theme-section, + .colorway-text, + .primary, + .checkbox-container:not(.multi-select-item), + .secondary, + .secondary-cta:not(.top), + .cta-link, + migration-wizard { + opacity: 0; + translate: 0 calc(-1 * var(--translate)); + } + + &:not(.dialog-initial) { + .steps:not(.progress-bar) { + opacity: 0.2; + } + } + } + } + + &.transition-out { + .noodle { + opacity: 0; + rotate: var(--rotate); + scale: var(--scale); + transition-delay: 0.2s; + } + + .screen:not(.dialog-last) { + .main-content { + overflow: hidden; + } + + .welcome-text, + .multi-select-container { + opacity: 0; + translate: 0 var(--translate); + transition-delay: 0.1s; + } + + // content that is nested between inner main content and navigation CTAs + // requires an additional 0.1s transition to avoid overlap + .tiles-theme-section, + .colorway-text, + migration-wizard { + opacity: 0; + translate: 0 var(--translate); + transition-delay: 0.2s; + } + + .primary, + .checkbox-container:not(.multi-select-item), + .secondary, + .secondary-cta:not(.top), + .cta-link { + opacity: 0; + translate: 0 var(--translate); + transition-delay: 0.3s; + } + + .steps:not(.progress-bar) { + opacity: 0.2; + transition-delay: 0.5s; + } + } + + .dialog-last { + .noodle { + transition-delay: 0s; + } + + .main-content, + .dismiss-button { + opacity: 0; + translate: 0 calc(2 * var(--translate)); + transition-delay: 0.4s; + } + } + } + + migration-wizard { + width: unset; + transition: var(--transition); + + &::part(buttons) { + margin-top: 32px; + justify-content: flex-start; + } + + &::part(deck) { + font-size: 0.83em; + } + } +} diff --git a/browser/components/newtab/content-src/aboutwelcome/components/AdditionalCTA.jsx b/browser/components/newtab/content-src/aboutwelcome/components/AdditionalCTA.jsx new file mode 100644 index 0000000000..2b61d1a82a --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/AdditionalCTA.jsx @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { Localized } from "./MSLocalized"; + +export const AdditionalCTA = ({ content, handleAction }) => { + let buttonStyle = ""; + + if (!content.additional_button?.style) { + buttonStyle = "primary"; + } else { + buttonStyle = + content.additional_button?.style === "link" + ? "cta-link" + : content.additional_button?.style; + } + + return ( + +

+ ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/EmbeddedMigrationWizard.jsx b/browser/components/newtab/content-src/aboutwelcome/components/EmbeddedMigrationWizard.jsx new file mode 100644 index 0000000000..43930009a5 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/EmbeddedMigrationWizard.jsx @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useEffect, useRef } from "react"; + +export const EmbeddedMigrationWizard = ({ handleAction }) => { + const ref = useRef(); + useEffect(() => { + const handleBeginMigration = () => { + handleAction({ + currentTarget: { value: "migrate_start" }, + source: "primary_button", + }); + }; + const handleClose = () => { + handleAction({ currentTarget: { value: "migrate_close" } }); + }; + const { current } = ref; + current?.addEventListener( + "MigrationWizard:BeginMigration", + handleBeginMigration + ); + current?.addEventListener("MigrationWizard:Close", handleClose); + return () => { + current?.removeEventListener( + "MigrationWizard:BeginMigration", + handleBeginMigration + ); + current?.removeEventListener("MigrationWizard:Close", handleClose); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + return ( + + + + ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/HelpText.jsx b/browser/components/newtab/content-src/aboutwelcome/components/HelpText.jsx new file mode 100644 index 0000000000..f7b413be81 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/HelpText.jsx @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { Localized } from "./MSLocalized"; +const MS_STRING_PROP = "string_id"; + +export const HelpText = props => { + if (!props.text) { + return null; + } + + if (props.hasImg) { + if (typeof props.text === "object" && props.text[MS_STRING_PROP]) { + return ( + +

+ +

+
+ ); + } else if (typeof props.text === "string") { + // Add the img at the end of the props.text + return ( +

+ {props.text} + +

+ ); + } + } else { + return ( + +

+ + ); + } + return null; +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/HeroImage.jsx b/browser/components/newtab/content-src/aboutwelcome/components/HeroImage.jsx new file mode 100644 index 0000000000..e03a5f84f4 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/HeroImage.jsx @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export const HeroImage = props => { + const { height, url, alt } = props; + + if (!url) { + return null; + } + + return ( +

+ {alt +
+ ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/LanguageSwitcher.jsx b/browser/components/newtab/content-src/aboutwelcome/components/LanguageSwitcher.jsx new file mode 100644 index 0000000000..9abd749a6d --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/LanguageSwitcher.jsx @@ -0,0 +1,294 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect } from "react"; +import { Localized } from "./MSLocalized"; +import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils"; + +/** + * The language switcher implements a hook that should be placed at a higher level + * than the actual language switcher component, as it needs to preemptively fetch + * and install langpacks for the user if there is a language mismatch screen. + */ +export function useLanguageSwitcher( + appAndSystemLocaleInfo, + screens, + screenIndex, + setScreenIndex +) { + const languageMismatchScreenIndex = screens.findIndex( + ({ id }) => id === "AW_LANGUAGE_MISMATCH" + ); + const screen = screens[languageMismatchScreenIndex]; + + // Ensure fluent messages have the negotiatedLanguage args set, as they are rendered + // before the negotiatedLanguage is known. If the arg isn't present then Firefox will + // crash in development mode. + useEffect(() => { + if (screen?.content?.languageSwitcher) { + for (const text of Object.values(screen.content.languageSwitcher)) { + if (text?.args && text.args.negotiatedLanguage === undefined) { + text.args.negotiatedLanguage = ""; + } + } + } + }, [screen]); + + // If there is a mismatch, then Firefox can negotiate a better langpack to offer + // the user. + const [negotiatedLanguage, setNegotiatedLanguage] = useState(null); + useEffect( + function getNegotiatedLanguage() { + if (!appAndSystemLocaleInfo) { + return; + } + if (appAndSystemLocaleInfo.matchType !== "language-mismatch") { + // There is no language mismatch, so there is no need to negotiate a langpack. + return; + } + + (async () => { + const { langPack, langPackDisplayName } = + await window.AWNegotiateLangPackForLanguageMismatch( + appAndSystemLocaleInfo + ); + if (langPack) { + setNegotiatedLanguage({ + langPackDisplayName, + appDisplayName: appAndSystemLocaleInfo.displayNames.appLanguage, + langPack, + requestSystemLocales: [ + langPack.target_locale, + appAndSystemLocaleInfo.appLocaleRaw, + ], + originalAppLocales: [appAndSystemLocaleInfo.appLocaleRaw], + }); + } else { + setNegotiatedLanguage({ + langPackDisplayName: null, + appDisplayName: null, + langPack: null, + requestSystemLocales: null, + }); + } + })(); + }, + [appAndSystemLocaleInfo] + ); + + /** + * @type { + * "before-installation" + * | "installing" + * | "installed" + * | "installation-error" + * | "none-available" + * } + */ + const [langPackInstallPhase, setLangPackInstallPhase] = useState( + "before-installation" + ); + useEffect( + function ensureLangPackInstalled() { + if (!negotiatedLanguage) { + // There are no negotiated languages to download yet. + return; + } + setLangPackInstallPhase("installing"); + window + .AWEnsureLangPackInstalled(negotiatedLanguage, screen?.content) + .then( + content => { + // Update screen content with strings that might have changed. + screen.content = content; + setLangPackInstallPhase("installed"); + }, + error => { + console.error(error); + setLangPackInstallPhase("installation-error"); + } + ); + }, + [negotiatedLanguage] + ); + + const [languageFilteredScreens, setLanguageFilteredScreens] = + useState(screens); + useEffect( + function filterScreen() { + // Remove the language screen if it exists (already removed for no live + // reload) and we either don't-need-to or can't switch. + if ( + screen && + (appAndSystemLocaleInfo?.matchType !== "language-mismatch" || + negotiatedLanguage?.langPack === null) + ) { + if (screenIndex > languageMismatchScreenIndex) { + setScreenIndex(screenIndex - 1); + } + setLanguageFilteredScreens( + screens.filter(s => s.id !== "AW_LANGUAGE_MISMATCH") + ); + } else { + setLanguageFilteredScreens(screens); + } + }, + [screens, negotiatedLanguage] + ); + + return { + negotiatedLanguage, + langPackInstallPhase, + languageFilteredScreens, + }; +} + +/** + * The language switcher is a separate component as it needs to perform some asynchronous + * network actions such as retrieving the list of langpacks available, and downloading + * a new langpack. On a fast connection, this won't be noticeable, but on slow or unreliable + * internet this may fail for a user. + */ +export function LanguageSwitcher(props) { + const { + content, + handleAction, + negotiatedLanguage, + langPackInstallPhase, + messageId, + } = props; + + const [isAwaitingLangpack, setIsAwaitingLangpack] = useState(false); + + // Determine the status of the langpack installation. + useEffect(() => { + if (isAwaitingLangpack && langPackInstallPhase !== "installing") { + window.AWSetRequestedLocales(negotiatedLanguage.requestSystemLocales); + requestAnimationFrame(() => { + handleAction( + // Simulate the click event. + { currentTarget: { value: "download_complete" } } + ); + }); + } + }, [isAwaitingLangpack, langPackInstallPhase]); + + let showWaitingScreen = false; + let showPreloadingScreen = false; + let showReadyScreen = false; + + if (isAwaitingLangpack && langPackInstallPhase !== "installed") { + showWaitingScreen = true; + } else if (langPackInstallPhase === "before-installation") { + showPreloadingScreen = true; + } else { + showReadyScreen = true; + } + + // Use {display: "none"} rather than if statements to prevent layout thrashing with + // the localized text elements rendering as blank, then filling in the text. + return ( +
+ {/* Pre-loading screen */} +
+ +
+ +
+
+ {/* Waiting to download the language screen. */} +
+ +
+ +
+
+ {/* The typical ready screen. */} +
+
+ +
+
+ +
+
+
+ ); +} diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MRColorways.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MRColorways.jsx new file mode 100644 index 0000000000..6a69f3483a --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/MRColorways.jsx @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect } from "react"; +import { Localized } from "./MSLocalized"; + +export const ColorwayDescription = props => { + const { colorway } = props; + if (!colorway) { + return null; + } + const { label, description } = colorway; + return ( + +
+ + ); +}; + +// Return colorway as "default" for default theme variations Automatic, Light, Dark, +// Alpenglow theme and legacy colorways which is not supported in Colorway picker. +// For themes other then default, theme names exist in +// format colorway-variationId inside LIGHT_WEIGHT_THEMES in AboutWelcomeParent +export function computeColorWay(themeName, systemVariations) { + return !themeName || + themeName === "alpenglow" || + systemVariations.includes(themeName) + ? "default" + : themeName.split("-")[0]; +} + +// Set variationIndex based off activetheme value e.g. 'light', 'expressionist-soft' +export function computeVariationIndex( + themeName, + systemVariations, + variations, + defaultVariationIndex +) { + // Check if themeName is in systemVariations, if yes choose variationIndex by themeName + let index = systemVariations.findIndex(theme => theme === themeName); + if (index >= 0) { + return index; + } + + // If themeName is one of the colorways, select variation index from colorways + let variation = themeName?.split("-")[1]; + index = variations.findIndex(element => element === variation); + if (index >= 0) { + return index; + } + return defaultVariationIndex; +} + +export function Colorways(props) { + let { + colorways, + darkVariation, + defaultVariationIndex, + systemVariations, + variations, + } = props.content.tiles; + let hasReverted = false; + + // Active theme id from JSON e.g. "expressionist" + const activeId = computeColorWay(props.activeTheme, systemVariations); + const [colorwayId, setState] = useState(activeId); + const [variationIndex, setVariationIndex] = useState(defaultVariationIndex); + + function revertToDefaultTheme() { + if (hasReverted) return; + + // Spoofing an event with current target value of "navigate_away" + // helps the handleAction method to read the colorways theme as "revert" + // which causes the initial theme to be activated. + // The "navigate_away" action is set in content in the colorways screen JSON config. + // Any value in the JSON for theme will work, provided it is not ``. + const event = { + currentTarget: { + value: "navigate_away", + }, + }; + props.handleAction(event); + hasReverted = true; + } + + // Revert to default theme if the user navigates away from the page or spotlight modal + // before clicking on the primary button to officially set theme. + useEffect(() => { + addEventListener("beforeunload", revertToDefaultTheme); + addEventListener("pagehide", revertToDefaultTheme); + + return () => { + removeEventListener("beforeunload", revertToDefaultTheme); + removeEventListener("pagehide", revertToDefaultTheme); + }; + }); + // Update state any time activeTheme changes. + useEffect(() => { + setState(computeColorWay(props.activeTheme, systemVariations)); + setVariationIndex( + computeVariationIndex( + props.activeTheme, + systemVariations, + variations, + defaultVariationIndex + ) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.activeTheme]); + + //select a random colorway + useEffect(() => { + //We don't want the default theme to be selected + const randomIndex = Math.floor(Math.random() * (colorways.length - 1)) + 1; + const randomColorwayId = colorways[randomIndex].id; + + // Change the variation to be the dark variation if configured and dark. + // Additional colorway changes will remain dark while system is unchanged. + if ( + darkVariation !== undefined && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + variations[variationIndex] = variations[darkVariation]; + } + const value = `${randomColorwayId}-${variations[variationIndex]}`; + props.handleAction({ currentTarget: { value } }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+
+ + + + {colorways.map(({ id, label, tooltip }) => ( + +
+
+ colorway.id === activeId)} + /> +
+ ); +} diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx new file mode 100644 index 0000000000..461f19fb28 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useEffect } from "react"; +const CONFIGURABLE_STYLES = [ + "color", + "fontSize", + "fontWeight", + "letterSpacing", + "lineHeight", + "marginBlock", + "marginInline", + "paddingBlock", + "paddingInline", +]; +const ZAP_SIZE_THRESHOLD = 160; + +/** + * Based on the .text prop, localizes an inner element if a string_id + * is provided, OR renders plain text, OR hides it if nothing is provided. + * Allows configuring of some styles including zap underline and color. + * + * Examples: + * + * Localized text + * ftl: + * title = Welcome + * jsx: + *

+ * output: + *

Welcome

+ * + * Unlocalized text + * jsx: + *

+ *

+ * output: + *

Welcome

+ */ + +export const Localized = ({ text, children }) => { + // Dynamically determine the size of the zap style. + const zapRef = React.createRef(); + useEffect(() => { + const { current } = zapRef; + if (current) + requestAnimationFrame(() => + current?.classList.replace( + "short", + current.getBoundingClientRect().width > ZAP_SIZE_THRESHOLD + ? "long" + : "short" + ) + ); + }); + + // Skip rendering of children with no text. + if (!text) { + return null; + } + + // Allow augmenting existing child container properties. + const props = { children: [], className: "", style: {}, ...children?.props }; + // Support nested Localized by starting with their children. + const textNodes = Array.isArray(props.children) + ? props.children + : [props.children]; + + // Pick desired fluent or raw/plain text to render. + if (text.string_id) { + // Set the key so React knows not to reuse when switching to plain text. + props.key = text.string_id; + props["data-l10n-id"] = text.string_id; + if (text.args) props["data-l10n-args"] = JSON.stringify(text.args); + } else if (text.raw) { + textNodes.push(text.raw); + } else if (typeof text === "string") { + textNodes.push(text); + } + + // Add zap style and content in a way that allows fluent to insert too. + if (text.zap) { + props.className += " welcomeZap"; + textNodes.push( + + {text.zap} + + ); + } + + if (text.aria_label) { + props["aria-label"] = text.aria_label; + } + + // Apply certain configurable styles. + CONFIGURABLE_STYLES.forEach(style => { + if (text[style] !== undefined) props.style[style] = text[style]; + }); + + return React.cloneElement( + // Provide a default container for the text if necessary. + children ?? , + props, + // Conditionally pass in as void elements can't accept empty array. + textNodes.length ? textNodes : null + ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MobileDownloads.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MobileDownloads.jsx new file mode 100644 index 0000000000..8390d2fd68 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/MobileDownloads.jsx @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { Localized } from "./MSLocalized"; + +export const MarketplaceButtons = props => { + return ( +
    + {props.buttons.includes("ios") ? ( +
  • + +
  • + ) : null} + {props.buttons.includes("android") ? ( +
  • + +
  • + ) : null} +
+ ); +}; + +export const MobileDownloads = props => { + const { QR_code: QRCode } = props.data; + const showEmailLink = + props.data.email && window.AWSendToDeviceEmailsSupported(); + + return ( +
+ {/* Avoid use of Localized element to set alt text here as a plain string value + results in a React error due to "dangerouslySetInnerHTML" */} + {QRCode ? ( + {typeof + ) : null} + {showEmailLink ? ( +
+ +
+ ) : null} + {props.data.marketplace_buttons ? ( + + ) : null} +
+ ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MultiSelect.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MultiSelect.jsx new file mode 100644 index 0000000000..0c1824215a --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/MultiSelect.jsx @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useEffect } from "react"; +import { Localized } from "./MSLocalized"; + +export const MultiSelect = props => { + let handleChange = event => { + if (event.currentTarget.checked) { + props.setActiveMultiSelect([ + ...props.activeMultiSelect, + event.currentTarget.value, + ]); + } else { + props.setActiveMultiSelect( + props.activeMultiSelect.filter(id => id !== event.currentTarget.value) + ); + } + }; + + let { data } = props.content.tiles; + // When screen renders for first time, update state + // with checkbox ids that has defaultvalue true + useEffect(() => { + if (!props.activeMultiSelect) { + props.setActiveMultiSelect( + data.map(item => item.defaultValue && item.id).filter(item => !!item) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ {props.content.tiles.data.map(({ label, id }) => ( +
+ + + + +
+ ))} +
+ ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx new file mode 100644 index 0000000000..b58510e514 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx @@ -0,0 +1,468 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect, useRef } from "react"; +import { Localized } from "./MSLocalized"; +import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils"; +import { MultiStageProtonScreen } from "./MultiStageProtonScreen"; +import { useLanguageSwitcher } from "./LanguageSwitcher"; +import { + BASE_PARAMS, + addUtmParams, +} from "../../asrouter/templates/FirstRun/addUtmParams"; + +// Amount of milliseconds for all transitions to complete (including delays). +const TRANSITION_OUT_TIME = 1000; +const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH"; + +export const MultiStageAboutWelcome = props => { + let { defaultScreens } = props; + const didFilter = useRef(false); + const [didMount, setDidMount] = useState(false); + const [screens, setScreens] = useState(defaultScreens); + + const [index, setScreenIndex] = useState(props.startScreen); + const [previousOrder, setPreviousOrder] = useState(props.startScreen - 1); + + useEffect(() => { + (async () => { + // If we want to load index from history state, we don't want to send impression yet + if (!didMount) { + return; + } + // On about:welcome first load, screensVisited should be empty + let screensVisited = didFilter.current ? screens.slice(0, index) : []; + let upcomingScreens = defaultScreens + .filter(s => !screensVisited.find(v => v.id === s.id)) + // Filter out Language Mismatch screen from upcoming + // screens if screens set from useLanguageSwitcher hook + // has filtered language screen + .filter( + upcomingScreen => + !( + !screens.find(s => s.id === LANGUAGE_MISMATCH_SCREEN_ID) && + upcomingScreen.id === LANGUAGE_MISMATCH_SCREEN_ID + ) + ); + + let filteredScreens = screensVisited.concat( + (await window.AWEvaluateScreenTargeting(upcomingScreens)) ?? + upcomingScreens + ); + + // Use existing screen for the filtered screen to carry over any modification + // e.g. if AW_LANGUAGE_MISMATCH exists, use it from existing screens + setScreens( + filteredScreens.map( + filtered => screens.find(s => s.id === filtered.id) ?? filtered + ) + ); + + didFilter.current = true; + + const screenInitials = filteredScreens + .map(({ id }) => id?.split("_")[1]?.[0]) + .join(""); + // Send impression ping when respective screen first renders + filteredScreens.forEach((screen, order) => { + if (index === order) { + AboutWelcomeUtils.sendImpressionTelemetry( + `${props.message_id}_${order}_${screen.id}_${screenInitials}` + ); + window.AWAddScreenImpression?.(screen); + } + }); + + // Remember that a new screen has loaded for browser navigation + if (props.updateHistory && index > window.history.state) { + window.history.pushState(index, ""); + } + + // Remember the previous screen index so we can animate the transition + setPreviousOrder(index); + })(); + }, [index, didMount]); // eslint-disable-line react-hooks/exhaustive-deps + + const [flowParams, setFlowParams] = useState(null); + const { metricsFlowUri } = props; + useEffect(() => { + (async () => { + if (metricsFlowUri) { + setFlowParams(await AboutWelcomeUtils.fetchFlowParams(metricsFlowUri)); + } + })(); + }, [metricsFlowUri]); + + // Allow "in" style to render to actually transition towards regular state, + // which also makes using browser back/forward navigation skip transitions. + const [transition, setTransition] = useState(props.transitions ? "in" : ""); + useEffect(() => { + if (transition === "in") { + requestAnimationFrame(() => + requestAnimationFrame(() => setTransition("")) + ); + } + }, [transition]); + + // Transition to next screen, opening about:home on last screen button CTA + const handleTransition = () => { + // Only handle transitioning out from a screen once. + if (transition === "out") { + return; + } + + // Start transitioning things "out" immediately when moving forwards. + setTransition(props.transitions ? "out" : ""); + + // Actually move forwards after all transitions finish. + setTimeout( + () => { + if (index < screens.length - 1) { + setTransition(props.transitions ? "in" : ""); + setScreenIndex(prevState => prevState + 1); + } else { + window.AWFinish(); + } + }, + props.transitions ? TRANSITION_OUT_TIME : 0 + ); + }; + + useEffect(() => { + // When about:welcome loads (on refresh or pressing back button + // from about:home), ensure history state usEffect runs before + // useEffect hook that send impression telemetry + setDidMount(true); + + if (props.updateHistory) { + // Switch to the screen tracked in state (null for initial state) + // or last screen index if a user navigates by pressing back + // button from about:home + const handler = ({ state }) => { + if (transition === "out") { + return; + } + setTransition(props.transitions ? "out" : ""); + setTimeout( + () => { + setTransition(props.transitions ? "in" : ""); + setScreenIndex(Math.min(state, screens.length - 1)); + }, + props.transitions ? TRANSITION_OUT_TIME : 0 + ); + }; + + // Handle page load, e.g., going back to about:welcome from about:home + const { state } = window.history; + if (state) { + setScreenIndex(Math.min(state, screens.length - 1)); + setPreviousOrder(Math.min(state, screens.length - 1)); + } + + // Watch for browser back/forward button navigation events + window.addEventListener("popstate", handler); + return () => window.removeEventListener("popstate", handler); + } + return false; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Save the active multi select state containing array of checkbox ids + // used in handleAction to update MULTI_ACTION data + const [activeMultiSelect, setActiveMultiSelect] = useState(null); + + // Get the active theme so the rendering code can make it selected + // by default. + const [activeTheme, setActiveTheme] = useState(null); + const [initialTheme, setInitialTheme] = useState(null); + useEffect(() => { + (async () => { + let theme = await window.AWGetSelectedTheme(); + setInitialTheme(theme); + setActiveTheme(theme); + })(); + }, []); + + const { negotiatedLanguage, langPackInstallPhase, languageFilteredScreens } = + useLanguageSwitcher( + props.appAndSystemLocaleInfo, + screens, + index, + setScreenIndex + ); + + useEffect(() => { + setScreens(languageFilteredScreens); + }, [languageFilteredScreens]); + + return ( + +
+ {screens.map((screen, order) => { + const isFirstScreen = screen === screens[0]; + const isLastScreen = screen === screens[screens.length - 1]; + const totalNumberOfScreens = screens.length; + const isSingleScreen = totalNumberOfScreens === 1; + + return index === order ? ( + + ) : null; + })} +
+
+ ); +}; + +export const SecondaryCTA = props => { + let targetElement = props.position + ? `secondary_button_${props.position}` + : `secondary_button`; + const buttonStyling = props.content.secondary_button?.has_arrow_icon + ? `secondary text-link arrow-icon` + : `secondary text-link`; + + return ( +
+ + + + +
+ ); +}; + +export const StepsIndicator = props => { + let steps = []; + for (let i = 0; i < props.totalNumberOfScreens; i++) { + let className = `${i === props.order ? "current" : ""} ${ + i < props.order ? "complete" : "" + }`; + steps.push( +
+ ); + } + return steps; +}; + +export const ProgressBar = ({ step, previousStep, totalNumberOfScreens }) => { + const [progress, setProgress] = React.useState( + previousStep / totalNumberOfScreens + ); + useEffect(() => { + // We don't need to hook any dependencies because any time the step changes, + // the screen's entire DOM tree will be re-rendered. + setProgress(step / totalNumberOfScreens); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + return ( +
+ ); +}; + +export class WelcomeScreen extends React.PureComponent { + constructor(props) { + super(props); + this.handleAction = this.handleAction.bind(this); + } + + handleOpenURL(action, flowParams, UTMTerm) { + let { type, data } = action; + if (type === "SHOW_FIREFOX_ACCOUNTS") { + let params = { + ...BASE_PARAMS, + utm_term: `${UTMTerm}-screen`, + }; + if (action.addFlowParams && flowParams) { + params = { + ...params, + ...flowParams, + }; + } + data = { ...data, extraParams: params }; + } else if (type === "OPEN_URL") { + let url = new URL(data.args); + addUtmParams(url, `${UTMTerm}-screen`); + if (action.addFlowParams && flowParams) { + url.searchParams.append("device_id", flowParams.deviceId); + url.searchParams.append("flow_id", flowParams.flowId); + url.searchParams.append("flow_begin_time", flowParams.flowBeginTime); + } + data = { ...data, args: url.toString() }; + } + return AboutWelcomeUtils.handleUserAction({ type, data }); + } + + async handleAction(event) { + let { props } = this; + const value = + event.currentTarget.value ?? event.currentTarget.getAttribute("value"); + const source = event.source || value; + let targetContent = + props.content[value] || + props.content.tiles || + props.content.languageSwitcher; + + if (!(targetContent && targetContent.action)) { + return; + } + // Send telemetry before waiting on actions + AboutWelcomeUtils.sendActionTelemetry(props.messageId, source, event.name); + + // Send additional telemetry if a messaging surface like feature callout is + // dismissed via the dismiss button. Other causes of dismissal will be + // handled separately by the messaging surface's own code. + if (value === "dismiss_button" && !event.name) { + AboutWelcomeUtils.sendDismissTelemetry(props.messageId, source); + } + + let { action } = targetContent; + + if (action.collectSelect) { + // Populate MULTI_ACTION data actions property with selected checkbox actions from tiles data + action.data = { + actions: [], + }; + + for (const checkbox of props.content?.tiles?.data ?? []) { + let checkboxAction; + if (this.props.activeMultiSelect.includes(checkbox.id)) { + checkboxAction = checkbox.checkedAction ?? checkbox.action; + } else { + checkboxAction = checkbox.uncheckedAction; + } + + if (checkboxAction) { + action.data.actions.push(checkboxAction); + } + } + + // Send telemetry with selected checkbox ids + AboutWelcomeUtils.sendActionTelemetry( + props.messageId, + props.activeMultiSelect, + "SELECT_CHECKBOX" + ); + } + + let actionResult; + if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) { + actionResult = await this.handleOpenURL( + action, + props.flowParams, + props.UTMTerm + ); + } else if (action.type) { + actionResult = await AboutWelcomeUtils.handleUserAction(action); + if (action.type === "FXA_SIGNIN_FLOW") { + AboutWelcomeUtils.sendActionTelemetry( + props.messageId, + actionResult ? "sign_in" : "sign_in_cancel", + "FXA_SIGNIN_FLOW" + ); + } + // Wait until migration closes to complete the action + const hasMigrate = a => + a.type === "SHOW_MIGRATION_WIZARD" || + (a.type === "MULTI_ACTION" && a.data?.actions?.some(hasMigrate)); + if (hasMigrate(action)) { + await window.AWWaitForMigrationClose(); + AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close"); + } + } + + // 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.currentTarget.value + : this.props.initialTheme || action.theme; + + this.props.setActiveTheme(themeToUse); + window.AWSelectTheme(themeToUse); + } + + // If the action has persistActiveTheme: true, we set the initial theme to the currently active theme + // so that it can be reverted to in the event that the user navigates away from the screen + if (action.persistActiveTheme) { + this.props.setInitialTheme(this.props.activeTheme); + } + + // `navigate` and `dismiss` can be true/false/undefined, or they can be a + // string "actionResult" in which case we should use the actionResult + // (boolean resolved by handleUserAction) + const shouldDoBehavior = behavior => + behavior === "actionResult" ? actionResult : behavior; + + if (shouldDoBehavior(action.navigate)) { + props.navigate(); + } + + if (shouldDoBehavior(action.dismiss)) { + window.AWFinish(); + } + } + + render() { + return ( + + ); + } +} diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MultiStageProtonScreen.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageProtonScreen.jsx new file mode 100644 index 0000000000..b51d2a2044 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageProtonScreen.jsx @@ -0,0 +1,472 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useEffect, useState } from "react"; +import { Localized } from "./MSLocalized"; +import { Colorways } from "./MRColorways"; +import { MobileDownloads } from "./MobileDownloads"; +import { MultiSelect } from "./MultiSelect"; +import { Themes } from "./Themes"; +import { + SecondaryCTA, + StepsIndicator, + ProgressBar, +} from "./MultiStageAboutWelcome"; +import { LanguageSwitcher } from "./LanguageSwitcher"; +import { CTAParagraph } from "./CTAParagraph"; +import { HeroImage } from "./HeroImage"; +import { OnboardingVideo } from "./OnboardingVideo"; +import { AdditionalCTA } from "./AdditionalCTA"; +import { EmbeddedMigrationWizard } from "./EmbeddedMigrationWizard"; + +export const MultiStageProtonScreen = props => { + const { autoAdvance, handleAction, order } = props; + useEffect(() => { + if (autoAdvance) { + const timer = setTimeout(() => { + handleAction({ + currentTarget: { + value: autoAdvance, + }, + name: "AUTO_ADVANCE", + }); + }, 20000); + return () => clearTimeout(timer); + } + return () => {}; + }, [autoAdvance, handleAction, order]); + + return ( + + ); +}; + +export const ProtonScreenActionButtons = props => { + const { content, addonName } = props; + const defaultValue = content.checkbox?.defaultValue; + + const [isChecked, setIsChecked] = useState(defaultValue || false); + + if ( + !content.primary_button && + !content.secondary_button && + !content.additional_button + ) { + return null; + } + + return ( +
+ +
+ ); +}; + +export class ProtonScreen extends React.PureComponent { + componentDidMount() { + this.mainContentHeader.focus(); + } + + getScreenClassName( + isFirstScreen, + isLastScreen, + includeNoodles, + isVideoOnboarding + ) { + const screenClass = `screen-${this.props.order % 2 !== 0 ? 1 : 2}`; + + if (isVideoOnboarding) return "with-video"; + + return `${isFirstScreen ? `dialog-initial` : ``} ${ + isLastScreen ? `dialog-last` : `` + } ${includeNoodles ? `with-noodles` : ``} ${screenClass}`; + } + + renderLogo({ + imageURL = "chrome://branding/content/about-logo.svg", + darkModeImageURL, + reducedMotionImageURL, + darkModeReducedMotionImageURL, + alt = "", + height, + }) { + return ( + + {darkModeReducedMotionImageURL ? ( + + ) : null} + {darkModeImageURL ? ( + + ) : null} + {reducedMotionImageURL ? ( + + ) : null} + {alt} + + ); + } + + renderContentTiles() { + const { content } = this.props; + return ( + + {content.tiles && + content.tiles.type === "colorway" && + content.tiles.colorways ? ( + + ) : null} + {content.tiles && + content.tiles.type === "theme" && + content.tiles.data ? ( + + ) : null} + {content.tiles && + content.tiles.type === "mobile_downloads" && + content.tiles.data ? ( + + ) : null} + {content.tiles && + content.tiles.type === "multiselect" && + content.tiles.data ? ( + + ) : null} + {content.tiles && content.tiles.type === "migration-wizard" ? ( + + ) : null} + + ); + } + + renderNoodles() { + return ( + +
+
+
+
+
+ + ); + } + + renderLanguageSwitcher() { + return this.props.content.languageSwitcher ? ( + + ) : null; + } + + renderDismissButton() { + return ( + + ); + } + + renderStepsIndicator() { + const currentStep = (this.props.order ?? 0) + 1; + const previousStep = (this.props.previousOrder ?? -1) + 1; + const { content, totalNumberOfScreens: total } = this.props; + return ( +
+ {content.progress_bar ? ( + + ) : ( + + )} +
+ ); + } + + renderSecondarySection(content) { + return ( +
+ +
+ + {content.hero_image ? ( + + ) : ( + +
+
+ +

+ +
+
+ + + + + )} +

+ ); + } + + render() { + const { + autoAdvance, + content, + isRtamo, + isTheme, + isFirstScreen, + isLastScreen, + isSingleScreen, + } = this.props; + const includeNoodles = content.has_noodles; + // The default screen position is "center" + const isCenterPosition = content.position === "center" || !content.position; + const hideStepsIndicator = + autoAdvance || content?.video_container || isSingleScreen; + const textColorClass = content.text_color + ? `${content.text_color}-text` + : ""; + // Assign proton screen style 'screen-1' or 'screen-2' to centered screens + // by checking if screen order is even or odd. + const screenClassName = isCenterPosition + ? this.getScreenClassName( + isFirstScreen, + isLastScreen, + includeNoodles, + content?.video_container + ) + : ""; + const isEmbeddedMigration = content.tiles?.type === "migration-wizard"; + + return ( +
{ + this.mainContentHeader = input; + }} + > + {isCenterPosition ? null : this.renderSecondarySection(content)} +
+ {content.secondary_button_top ? ( + + ) : null} + {includeNoodles ? this.renderNoodles() : null} +
+ {content.logo ? this.renderLogo(content.logo) : null} + + {isRtamo ? ( +
+ +
+ ) : null} + +
+
+ {content.title ? ( + +

+ + ) : null} + {content.subtitle ? ( + +

+ + ) : null} + {content.cta_paragraph ? ( + + ) : null} +

+ {content.video_container ? ( + + ) : null} + {this.renderContentTiles()} + {this.renderLanguageSwitcher()} + +
+ {!hideStepsIndicator ? this.renderStepsIndicator() : null} +
+ {content.dismiss_button ? this.renderDismissButton() : null} +
+
+ ); + } +} diff --git a/browser/components/newtab/content-src/aboutwelcome/components/OnboardingVideo.jsx b/browser/components/newtab/content-src/aboutwelcome/components/OnboardingVideo.jsx new file mode 100644 index 0000000000..629a409a59 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/OnboardingVideo.jsx @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export const OnboardingVideo = props => { + const vidUrl = props.content.video_url; + const autoplay = props.content.autoPlay; + + const handleVideoAction = event => { + props.handleAction({ + currentTarget: { + value: event, + }, + }); + }; + + return ( +
+ +
+ ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx b/browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx new file mode 100644 index 0000000000..fb5e80c6e4 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { + AboutWelcomeUtils, + DEFAULT_RTAMO_CONTENT, +} from "../../lib/aboutwelcome-utils"; +import { MultiStageProtonScreen } from "./MultiStageProtonScreen"; +import { BASE_PARAMS } from "../../asrouter/templates/FirstRun/addUtmParams"; + +export class ReturnToAMO extends React.PureComponent { + constructor(props) { + super(props); + this.fetchFlowParams = this.fetchFlowParams.bind(this); + this.handleAction = this.handleAction.bind(this); + } + + async fetchFlowParams() { + if (this.props.metricsFlowUri) { + this.setState({ + flowParams: await AboutWelcomeUtils.fetchFlowParams( + this.props.metricsFlowUri + ), + }); + } + } + + componentDidUpdate() { + this.fetchFlowParams(); + } + + handleAction(event) { + const { content, message_id, url, utm_term } = this.props; + let { action, source_id } = content[event.currentTarget.value]; + let { type, data } = action; + + if (type === "INSTALL_ADDON_FROM_URL") { + if (!data) { + return; + } + // Set add-on url in action.data.url property from JSON + data = { ...data, url }; + } else if (type === "SHOW_FIREFOX_ACCOUNTS") { + let params = { + ...BASE_PARAMS, + utm_term: `aboutwelcome-${utm_term}-screen`, + }; + if (action.addFlowParams && this.state.flowParams) { + params = { + ...params, + ...this.state.flowParams, + }; + } + data = { ...data, extraParams: params }; + } + + AboutWelcomeUtils.handleUserAction({ type, data }); + AboutWelcomeUtils.sendActionTelemetry(message_id, source_id); + } + + render() { + const { content, type } = this.props; + + if (!content) { + return null; + } + + if (content?.primary_button.label) { + content.primary_button.label.string_id = type.includes("theme") + ? "return-to-amo-add-theme-label" + : "mr1-return-to-amo-add-extension-label"; + } + + // For experiments, when needed below rendered UI allows settings hard coded strings + // directly inside JSON except for ReturnToAMOText which picks add-on name and icon from fluent string + return ( +
+ +
+ ); + } +} + +ReturnToAMO.defaultProps = DEFAULT_RTAMO_CONTENT; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/Themes.jsx b/browser/components/newtab/content-src/aboutwelcome/components/Themes.jsx new file mode 100644 index 0000000000..def2603426 --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/Themes.jsx @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { Localized } from "./MSLocalized"; + +export const Themes = props => { + return ( +
+
+
+ + + + {props.content.tiles.data.map( + ({ theme, label, tooltip, description }) => ( + +
+
+
+ ); +}; diff --git a/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx b/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx new file mode 100644 index 0000000000..a067c4d7fe --- /dev/null +++ b/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useEffect } from "react"; +import { Localized } from "./MSLocalized"; +const MS_STRING_PROP = "string_id"; +const ZAP_SIZE_THRESHOLD = 160; + +function calculateZapLength() { + let span = document.querySelector(".zap"); + if (!span) { + return; + } + let rect = span.getBoundingClientRect(); + if (rect && rect.width > ZAP_SIZE_THRESHOLD) { + span.classList.add("long"); + } else { + span.classList.add("short"); + } +} + +export const Zap = props => { + useEffect(() => { + requestAnimationFrame(() => calculateZapLength()); + }); + + if (!props.text) { + return null; + } + + if (props.hasZap) { + if (typeof props.text === "object" && props.text[MS_STRING_PROP]) { + return ( + +

+ +

+
+ ); + } else if (typeof props.text === "string") { + // Parse string to zap style last word of the props.text + let titleArray = props.text.split(" "); + let lastWord = `${titleArray.pop()}`; + return ( +

+ {titleArray.join(" ").concat(" ")} + {lastWord} +

+ ); + } + } else { + return ( + +

+ + ); + } + return null; +}; diff --git a/browser/components/newtab/content-src/activity-stream.jsx b/browser/components/newtab/content-src/activity-stream.jsx new file mode 100644 index 0000000000..c588e8e850 --- /dev/null +++ b/browser/components/newtab/content-src/activity-stream.jsx @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { Base } from "content-src/components/Base/Base"; +import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; +import { initStore } from "content-src/lib/init-store"; +import { Provider } from "react-redux"; +import React from "react"; +import ReactDOM from "react-dom"; +import { reducers } from "common/Reducers.sys.mjs"; + +export const NewTab = ({ store }) => ( + + + +); + +export function renderWithoutState() { + const store = initStore(reducers); + new DetectUserSessionStart(store).sendEventOrAddListener(); + + // If this document has already gone into the background by the time we've reached + // here, we can deprioritize requesting the initial state until the event loop + // frees up. If, however, the visibility changes, we then send the request. + let didRequest = false; + let requestIdleCallbackId = 0; + function doRequest() { + if (!didRequest) { + if (requestIdleCallbackId) { + cancelIdleCallback(requestIdleCallbackId); + } + didRequest = true; + store.dispatch(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); + } + } + + if (document.hidden) { + requestIdleCallbackId = requestIdleCallback(doRequest); + addEventListener("visibilitychange", doRequest, { once: true }); + } else { + doRequest(); + } + + ReactDOM.hydrate(, document.getElementById("root")); +} + +export function renderCache(initialState) { + const store = initStore(reducers, initialState); + new DetectUserSessionStart(store).sendEventOrAddListener(); + + ReactDOM.hydrate(, document.getElementById("root")); +} diff --git a/browser/components/newtab/content-src/asrouter/README.md b/browser/components/newtab/content-src/asrouter/README.md new file mode 100644 index 0000000000..0ee3345630 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/README.md @@ -0,0 +1,34 @@ +# Activity Stream Router + +## Preferences `browser.newtab.activity-stream.asrouter.*` + +Name | Used for | Type | Example value +--- | --- | --- | --- +`allowHosts` | Allow a host in order to fetch messages from its endpoint | `[String]` | `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]` +`providers.snippets` | Message provider options for snippets | `Object` | [see below](#message-providers) +`providers.cfr` | Message provider options for cfr | `Object` | [see below](#message-providers) +`providers.onboarding` | Message provider options for onboarding | `Object` | [see below](#message-providers) +`useRemoteL10n` | Controls whether to use the remote Fluent files for l10n, default as `true` | `Boolean` | `[true|false]` + +### Message providers examples + +```json +{ + "id" : "snippets", + "type" : "remote", + "enabled": true, + "url" : "https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json", + "updateCycleInMs" : 14400000 +} +``` + +```json +{ + "id" : "onboarding", + "enabled": true, + "type" : "local", + "localProvider" : "OnboardingMessageProvider" +} +``` + +### [Snippet message format documentation](https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/schemas/message-format.md) diff --git a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx new file mode 100644 index 0000000000..4c3f08d4cd --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx @@ -0,0 +1,346 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { MESSAGE_TYPE_HASH as msg } from "common/ActorConstants.sys.mjs"; +import { actionTypes as at } from "common/Actions.sys.mjs"; +import { ASRouterUtils } from "./asrouter-utils"; +import { generateBundles } from "./rich-text-strings"; +import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper"; +import { LocalizationProvider, ReactLocalization } from "@fluent/react"; +import { NEWTAB_DARK_THEME } from "content-src/lib/constants"; +import React from "react"; +import ReactDOM from "react-dom"; +import { SnippetsTemplates } from "./templates/template-manifest"; + +const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"]; + +// Note: nextProps/prevProps refer to props passed to , not +function shouldSendImpressionOnUpdate(nextProps, prevProps) { + return ( + nextProps.message.id && + (!prevProps.message || prevProps.message.id !== nextProps.message.id) + ); +} + +export class ASRouterUISurface extends React.PureComponent { + constructor(props) { + super(props); + this.sendClick = this.sendClick.bind(this); + this.sendImpression = this.sendImpression.bind(this); + this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this); + this.onUserAction = this.onUserAction.bind(this); + this.fetchFlowParams = this.fetchFlowParams.bind(this); + this.onBlockSelected = this.onBlockSelected.bind(this); + this.onBlockById = this.onBlockById.bind(this); + this.onDismiss = this.onDismiss.bind(this); + this.onMessageFromParent = this.onMessageFromParent.bind(this); + + this.state = { message: {} }; + if (props.document) { + this.footerPortal = props.document.getElementById( + "footer-asrouter-container" + ); + } + } + + async fetchFlowParams(params = {}) { + let result = {}; + const { fxaEndpoint } = this.props; + if (!fxaEndpoint) { + const err = + "Tried to fetch flow params before fxaEndpoint pref was ready"; + console.error(err); + } + + try { + const urlObj = new URL(fxaEndpoint); + urlObj.pathname = "metrics-flow"; + Object.keys(params).forEach(key => { + urlObj.searchParams.append(key, params[key]); + }); + const response = await fetch(urlObj.toString(), { credentials: "omit" }); + if (response.status === 200) { + const { deviceId, flowId, flowBeginTime } = await response.json(); + result = { deviceId, flowId, flowBeginTime }; + } else { + console.error("Non-200 response", response); + } + } catch (error) { + console.error(error); + } + return result; + } + + sendUserActionTelemetry(extraProps = {}) { + const { message } = this.state; + const eventType = `${message.provider}_user_event`; + const source = extraProps.id; + delete extraProps.id; + ASRouterUtils.sendTelemetry({ + source, + message_id: message.id, + action: eventType, + ...extraProps, + }); + } + + sendImpression(extraProps) { + if (this.state.message.provider === "preview") { + return Promise.resolve(); + } + + this.sendUserActionTelemetry({ event: "IMPRESSION", ...extraProps }); + return ASRouterUtils.sendMessage({ + type: msg.IMPRESSION, + data: this.state.message, + }); + } + + // If link has a `metric` data attribute send it as part of the `event_context` + // telemetry field which can have arbitrary values. + // Used for router messages with links as part of the content. + sendClick(event) { + const { dataset } = event.target; + const metric = { + event_context: dataset.metric, + // Used for the `source` of the event. Needed to differentiate + // from other snippet or onboarding events that may occur. + id: "NEWTAB_FOOTER_BAR_CONTENT", + }; + const { entrypoint_name, entrypoint_value } = dataset; + // Assign the snippet referral for the action + const entrypoint = entrypoint_name + ? new URLSearchParams([[entrypoint_name, entrypoint_value]]).toString() + : entrypoint_value; + const action = { + type: dataset.action, + data: { + args: dataset.args, + ...(entrypoint && { entrypoint }), + }, + }; + if (action.type) { + ASRouterUtils.executeAction(action); + } + if ( + !this.state.message.content.do_not_autoblock && + !dataset.do_not_autoblock + ) { + this.onBlockById(this.state.message.id); + } + if (this.state.message.provider !== "preview") { + this.sendUserActionTelemetry({ event: "CLICK_BUTTON", ...metric }); + } + } + + onBlockSelected(options) { + return this.onBlockById(this.state.message.id, { + ...options, + campaign: this.state.message.campaign, + }); + } + + onBlockById(id, options) { + return ASRouterUtils.blockById(id, options).then(clearAll => { + if (clearAll) { + this.setState({ message: {} }); + } + }); + } + + onDismiss() { + this.clearMessage(this.state.message.id); + } + + // Blocking a snippet by id blocks the entire campaign + // so when clearing we use the two values interchangeably + clearMessage(idOrCampaign) { + if ( + idOrCampaign === this.state.message.id || + idOrCampaign === this.state.message.campaign + ) { + this.setState({ message: {} }); + } + } + + clearProvider(id) { + if (this.state.message.provider === id) { + this.setState({ message: {} }); + } + } + + onMessageFromParent({ type, data }) { + // These only exists due to onPrefChange events in ASRouter + switch (type) { + case "ClearMessages": { + data.forEach(id => this.clearMessage(id)); + break; + } + case "ClearProviders": { + data.forEach(id => this.clearProvider(id)); + break; + } + case "EnterSnippetsPreviewMode": { + this.props.dispatch({ type: at.SNIPPETS_PREVIEW_MODE }); + break; + } + } + } + + requestMessage(endpoint) { + ASRouterUtils.sendMessage({ + type: "NEWTAB_MESSAGE_REQUEST", + data: { endpoint }, + }).then(state => this.setState(state)); + } + + componentWillMount() { + const endpoint = ASRouterUtils.getPreviewEndpoint(); + if (endpoint && endpoint.theme === "dark") { + global.window.dispatchEvent( + new CustomEvent("LightweightTheme:Set", { + detail: { data: NEWTAB_DARK_THEME }, + }) + ); + } + if (endpoint && endpoint.dir === "rtl") { + //Set `dir = rtl` on the HTML + this.props.document.dir = "rtl"; + } + ASRouterUtils.addListener(this.onMessageFromParent); + this.requestMessage(endpoint); + } + + componentWillUnmount() { + ASRouterUtils.removeListener(this.onMessageFromParent); + } + + componentDidUpdate(prevProps, prevState) { + if ( + prevProps.adminContent && + JSON.stringify(prevProps.adminContent) !== + JSON.stringify(this.props.adminContent) + ) { + this.updateContent(); + } + if (prevState.message.id !== this.state.message.id) { + const main = global.window.document.querySelector("main"); + if (main) { + if (this.state.message.id) { + main.classList.add("has-snippet"); + } else { + main.classList.remove("has-snippet"); + } + } + } + } + + updateContent() { + this.setState({ + ...this.props.adminContent, + }); + } + + async getMonitorUrl({ url, flowRequestParams = {} }) { + const flowValues = await this.fetchFlowParams(flowRequestParams); + + // Note that flowParams are actually added dynamically on the page + const urlObj = new URL(url); + ["deviceId", "flowId", "flowBeginTime"].forEach(key => { + if (key in flowValues) { + urlObj.searchParams.append(key, flowValues[key]); + } + }); + + return urlObj.toString(); + } + + async onUserAction(action) { + switch (action.type) { + // This needs to be handled locally because its + case "ENABLE_FIREFOX_MONITOR": + const url = await this.getMonitorUrl(action.data.args); + ASRouterUtils.executeAction({ type: "OPEN_URL", data: { args: url } }); + break; + default: + ASRouterUtils.executeAction(action); + } + } + + renderSnippets() { + const { message } = this.state; + if (!SnippetsTemplates[message.template]) { + return null; + } + const SnippetComponent = SnippetsTemplates[message.template]; + const { content } = message; + + return ( + + + + + + ); + } + + renderPreviewBanner() { + if (this.state.message.provider !== "preview") { + return null; + } + + return ( +
+ + Preview Purposes Only +
+ ); + } + + render() { + const { message } = this.state; + if (!message.id) { + return null; + } + const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes( + message.template + ); + + return shouldRenderBelowSearch ? ( + // Render special below search snippets in place; +
+ {this.renderSnippets()} +
+ ) : ( + // For regular snippets etc. we should render everything in our footer + // container. + ReactDOM.createPortal( + <> + {this.renderPreviewBanner()} + {this.renderSnippets()} + , + this.footerPortal + ) + ); + } +} + +ASRouterUISurface.defaultProps = { document: global.document }; diff --git a/browser/components/newtab/content-src/asrouter/asrouter-utils.js b/browser/components/newtab/content-src/asrouter/asrouter-utils.js new file mode 100644 index 0000000000..9864823a77 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/asrouter-utils.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { MESSAGE_TYPE_HASH as msg } from "common/ActorConstants.sys.mjs"; +import { actionCreators as ac } from "common/Actions.sys.mjs"; + +export const ASRouterUtils = { + addListener(listener) { + if (global.ASRouterAddParentListener) { + global.ASRouterAddParentListener(listener); + } + }, + removeListener(listener) { + if (global.ASRouterRemoveParentListener) { + global.ASRouterRemoveParentListener(listener); + } + }, + sendMessage(action) { + if (global.ASRouterMessage) { + return global.ASRouterMessage(action); + } + throw new Error(`Unexpected call:\n${JSON.stringify(action, null, 3)}`); + }, + blockById(id, options) { + return ASRouterUtils.sendMessage({ + type: msg.BLOCK_MESSAGE_BY_ID, + data: { id, ...options }, + }); + }, + modifyMessageJson(content) { + return ASRouterUtils.sendMessage({ + type: msg.MODIFY_MESSAGE_JSON, + data: { content }, + }); + }, + executeAction(button_action) { + return ASRouterUtils.sendMessage({ + type: msg.USER_ACTION, + data: button_action, + }); + }, + unblockById(id) { + return ASRouterUtils.sendMessage({ + type: msg.UNBLOCK_MESSAGE_BY_ID, + data: { id }, + }); + }, + blockBundle(bundle) { + return ASRouterUtils.sendMessage({ + type: msg.BLOCK_BUNDLE, + data: { bundle }, + }); + }, + unblockBundle(bundle) { + return ASRouterUtils.sendMessage({ + type: msg.UNBLOCK_BUNDLE, + data: { bundle }, + }); + }, + overrideMessage(id) { + return ASRouterUtils.sendMessage({ + type: msg.OVERRIDE_MESSAGE, + data: { id }, + }); + }, + sendTelemetry(ping) { + return ASRouterUtils.sendMessage(ac.ASRouterUserEvent(ping)); + }, + getPreviewEndpoint() { + if ( + global.document && + global.document.location && + global.document.location.href.includes("endpoint") + ) { + const params = new URLSearchParams( + global.document.location.href.slice( + global.document.location.href.indexOf("endpoint") + ) + ); + try { + const endpoint = new URL(params.get("endpoint")); + return { + url: endpoint.href, + snippetId: params.get("snippetId"), + theme: this.getPreviewTheme(), + dir: this.getPreviewDir(), + }; + } catch (e) {} + } + + return null; + }, + getPreviewTheme() { + return new URLSearchParams( + global.document.location.href.slice( + global.document.location.href.indexOf("theme") + ) + ).get("theme"); + }, + getPreviewDir() { + return new URLSearchParams( + global.document.location.href.slice( + global.document.location.href.indexOf("dir") + ) + ).get("dir"); + }, +}; diff --git a/browser/components/newtab/content-src/asrouter/components/Button/Button.jsx b/browser/components/newtab/content-src/asrouter/components/Button/Button.jsx new file mode 100644 index 0000000000..b3ece86f16 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/Button/Button.jsx @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +const ALLOWED_STYLE_TAGS = ["color", "backgroundColor"]; + +export const Button = props => { + const style = {}; + + // Add allowed style tags from props, e.g. props.color becomes style={color: props.color} + for (const tag of ALLOWED_STYLE_TAGS) { + if (typeof props[tag] !== "undefined") { + style[tag] = props[tag]; + } + } + // remove border if bg is set to something custom + if (style.backgroundColor) { + style.border = "0"; + } + + return ( + + ); +}; diff --git a/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss b/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss new file mode 100644 index 0000000000..35234be4b0 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss @@ -0,0 +1,51 @@ +.ASRouterButton { + font-weight: 600; + font-size: 14px; + white-space: nowrap; + border-radius: 2px; + border: 0; + font-family: inherit; + padding: 8px 15px; + margin-inline-start: 12px; + color: inherit; + cursor: pointer; + + .tall & { + margin-inline-start: 20px; + } + + &.test-only { + width: 0; + height: 0; + overflow: hidden; + display: block; + visibility: hidden; + } + + &.primary { + border: 1px solid var(--newtab-primary-action-background); + background-color: var(--newtab-primary-action-background); + color: var(--newtab-primary-element-text-color); + + &:hover { + background-color: var(--newtab-primary-element-hover-color); + } + + &:active { + background-color: var(--newtab-primary-element-active-color); + } + } + + &.slim { + border: $border-primary; + margin-inline-start: 0; + font-size: 12px; + padding: 6px 12px; + + &:hover, + &:focus { + box-shadow: $shadow-primary; + transition: box-shadow 150ms; + } + } +} diff --git a/browser/components/newtab/content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx b/browser/components/newtab/content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx new file mode 100644 index 0000000000..e4b0812f26 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// lifted from https://gist.github.com/kitze/23d82bb9eb0baabfd03a6a720b1d637f +const ConditionalWrapper = ({ condition, wrap, children }) => + condition && wrap ? wrap(children) : children; + +export default ConditionalWrapper; diff --git a/browser/components/newtab/content-src/asrouter/components/ImpressionsWrapper/ImpressionsWrapper.jsx b/browser/components/newtab/content-src/asrouter/components/ImpressionsWrapper/ImpressionsWrapper.jsx new file mode 100644 index 0000000000..8498bde03b --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/ImpressionsWrapper/ImpressionsWrapper.jsx @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export const VISIBLE = "visible"; +export const VISIBILITY_CHANGE_EVENT = "visibilitychange"; + +/** + * Component wrapper used to send telemetry pings on every impression. + */ +export class ImpressionsWrapper extends React.PureComponent { + // This sends an event when a user sees a set of new content. If content + // changes while the page is hidden (i.e. preloaded or on a hidden tab), + // only send the event if the page becomes visible again. + sendImpressionOrAddListener() { + if (this.props.document.visibilityState === VISIBLE) { + this.props.sendImpression({ id: this.props.id }); + } else { + // We should only ever send the latest impression stats ping, so remove any + // older listeners. + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + + // When the page becomes visible, send the impression stats ping if the section isn't collapsed. + this._onVisibilityChange = () => { + if (this.props.document.visibilityState === VISIBLE) { + this.props.sendImpression({ id: this.props.id }); + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + }; + this.props.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + componentWillUnmount() { + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + componentDidMount() { + if (this.props.sendOnMount) { + this.sendImpressionOrAddListener(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.shouldSendImpressionOnUpdate(this.props, prevProps)) { + this.sendImpressionOrAddListener(); + } + } + + render() { + return this.props.children; + } +} + +ImpressionsWrapper.defaultProps = { + document: global.document, + sendOnMount: true, +}; diff --git a/browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx b/browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx new file mode 100644 index 0000000000..fdfdf22db2 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export class ModalOverlayWrapper extends React.PureComponent { + constructor(props) { + super(props); + this.onKeyDown = this.onKeyDown.bind(this); + } + + // The intended behaviour is to listen for an escape key + // but not for a click; see Bug 1582242 + onKeyDown(event) { + if (event.key === "Escape") { + this.props.onClose(event); + } + } + + componentWillMount() { + this.props.document.addEventListener("keydown", this.onKeyDown); + this.props.document.body.classList.add("modal-open"); + } + + componentWillUnmount() { + this.props.document.removeEventListener("keydown", this.onKeyDown); + this.props.document.body.classList.remove("modal-open"); + } + + render() { + const { props } = this; + let className = props.unstyled ? "" : "modalOverlayInner active"; + if (props.innerClassName) { + className += ` ${props.innerClassName}`; + } + return ( +
+ +
+ ); + } +} + +ModalOverlayWrapper.defaultProps = { document: global.document }; diff --git a/browser/components/newtab/content-src/asrouter/components/ModalOverlay/_ModalOverlay.scss b/browser/components/newtab/content-src/asrouter/components/ModalOverlay/_ModalOverlay.scss new file mode 100644 index 0000000000..a1006c9437 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/ModalOverlay/_ModalOverlay.scss @@ -0,0 +1,103 @@ +// Variable for the about:welcome modal scrollbars +$modal-scrollbar-z-index: 1100; + +.activity-stream { + &.modal-open { + overflow: hidden; + } +} + +.modalOverlayOuter { + background: var(--newtab-overlay-color); + height: 100%; + position: fixed; + top: 0; + left: 0; + width: 100%; + display: none; + z-index: $modal-scrollbar-z-index; + overflow: auto; + + &.active { + display: flex; + } +} + +.modalOverlayInner { + min-width: min-content; + width: 100%; + max-width: 960px; + position: relative; + margin: auto; + background: var(--newtab-background-color-secondary); + box-shadow: $shadow-large; + border-radius: 4px; + display: none; + z-index: $modal-scrollbar-z-index; + + // modal takes over entire screen + @media(max-width: 960px) { + height: 100%; + top: 0; + left: 0; + box-shadow: none; + border-radius: 0; + } + + &.active { + display: block; + } + + h2 { + color: var(--newtab-text-primary-color); + text-align: center; + font-weight: 200; + margin-top: 30px; + font-size: 28px; + line-height: 37px; + letter-spacing: -0.13px; + + @media(max-width: 960px) { + margin-top: 100px; + } + + @media(max-width: 850px) { + margin-top: 30px; + } + } + + .footer { + border-top: $border-secondary; + border-radius: 4px; + height: 70px; + width: 100%; + position: absolute; + bottom: 0; + text-align: center; + background-color: $white; + + // if modal is short enough, footer becomes sticky + @media(max-width: 850px) and (max-height: 730px) { + position: sticky; + } + + // if modal is narrow enough, footer becomes sticky + @media(max-width: 650px) and (max-height: 600px) { + position: sticky; + } + + .modalButton { + margin-top: 20px; + min-width: 150px; + height: 30px; + padding: 4px 30px 6px; + font-size: 15px; + + &:focus, + &.active, + &:hover { + @include fade-in-card; + } + } + } +} diff --git a/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx b/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx new file mode 100644 index 0000000000..d430fa5c3d --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Localized } from "@fluent/react"; +import React from "react"; +import { RICH_TEXT_KEYS } from "../../rich-text-strings"; +import { safeURI } from "../../template-utils"; + +// Elements allowed in snippet content +const ALLOWED_TAGS = { + b: , + i: , + u: , + strong: , + em: , + br:
, +}; + +/** + * Transform an object (tag name: {url}) into (tag name: anchor) where the url + * is used as href, in order to render links inside a Fluent.Localized component. + */ +export function convertLinks( + links, + sendClick, + doNotAutoBlock, + openNewWindow = false +) { + if (links) { + return Object.keys(links).reduce((acc, linkTag) => { + const { action } = links[linkTag]; + // Setting the value to false will not include the attribute in the anchor + const url = action ? false : safeURI(links[linkTag].url); + + acc[linkTag] = ( + // eslint was getting a false positive caused by the dynamic injection + // of content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ); + return acc; + }, {}); + } + + return null; +} + +/** + * Message wrapper used to sanitize markup and render HTML. + */ +export function RichText(props) { + if (!RICH_TEXT_KEYS.includes(props.localization_id)) { + throw new Error( + `ASRouter: ${props.localization_id} is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js` + ); + } + return ( + + {props.text} + + ); +} diff --git a/browser/components/newtab/content-src/asrouter/components/SnippetBase/SnippetBase.jsx b/browser/components/newtab/content-src/asrouter/components/SnippetBase/SnippetBase.jsx new file mode 100644 index 0000000000..fd25337fbf --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/components/SnippetBase/SnippetBase.jsx @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export class SnippetBase extends React.PureComponent { + constructor(props) { + super(props); + this.onBlockClicked = this.onBlockClicked.bind(this); + this.onDismissClicked = this.onDismissClicked.bind(this); + this.setBlockButtonRef = this.setBlockButtonRef.bind(this); + this.onBlockButtonMouseEnter = this.onBlockButtonMouseEnter.bind(this); + this.onBlockButtonMouseLeave = this.onBlockButtonMouseLeave.bind(this); + this.state = { blockButtonHover: false }; + } + + componentDidMount() { + if (this.blockButtonRef) { + this.blockButtonRef.addEventListener( + "mouseenter", + this.onBlockButtonMouseEnter + ); + this.blockButtonRef.addEventListener( + "mouseleave", + this.onBlockButtonMouseLeave + ); + } + } + + componentWillUnmount() { + if (this.blockButtonRef) { + this.blockButtonRef.removeEventListener( + "mouseenter", + this.onBlockButtonMouseEnter + ); + this.blockButtonRef.removeEventListener( + "mouseleave", + this.onBlockButtonMouseLeave + ); + } + } + + setBlockButtonRef(element) { + this.blockButtonRef = element; + } + + onBlockButtonMouseEnter() { + this.setState({ blockButtonHover: true }); + } + + onBlockButtonMouseLeave() { + this.setState({ blockButtonHover: false }); + } + + onBlockClicked() { + if (this.props.provider !== "preview") { + this.props.sendUserActionTelemetry({ + event: "BLOCK", + id: this.props.UISurface, + }); + } + + this.props.onBlock(); + } + + onDismissClicked() { + if (this.props.provider !== "preview") { + this.props.sendUserActionTelemetry({ + event: "DISMISS", + id: this.props.UISurface, + }); + } + + this.props.onDismiss(); + } + + renderDismissButton() { + if (this.props.footerDismiss) { + return ( +
+
+ +
+
+ ); + } + + const label = this.props.content.block_button_text || "Remove this"; + return ( + + + ); + } + + render() { + const textStyle = { + color: this.props.content.text_color, + backgroundColor: this.props.content.background_color, + }; + const customElement = ( + + ); + return ( + + ); + } +} + +export const EOYSnippet = props => { + const extendedContent = { + monthly_checkbox_label_text: "Make my donation monthly", + locale: "en-US", + currency_code: "usd", + selected_button: "donation_amount_second", + ...props.content, + }; + + return ( + + ); +}; diff --git a/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json new file mode 100644 index 0000000000..d9b6728067 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json @@ -0,0 +1,171 @@ +{ + "title": "EOYSnippet", + "description": "Fundraising Snippet", + "version": "1.1.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "donation_form_url": { + "type": "string", + "description": "Url to the donation form." + }, + "currency_code": { + "type": "string", + "description": "The code for the currency. Examle gbp, cad, usd.", + "default": "usd" + }, + "locale": { + "type": "string", + "description": "String for the locale code.", + "default": "en-US" + }, + "text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "text_color": { + "type": "string", + "description": "Modify the text message color" + }, + "background_color": { + "type": "string", + "description": "Snippet background color." + }, + "highlight_color": { + "type": "string", + "description": "Paragraph em highlight color." + }, + "donation_amount_first": { + "type": "number", + "description": "First button amount." + }, + "donation_amount_second": { + "type": "number", + "description": "Second button amount." + }, + "donation_amount_third": { + "type": "number", + "description": "Third button amount." + }, + "donation_amount_fourth": { + "type": "number", + "description": "Fourth button amount." + }, + "selected_button": { + "type": "string", + "description": "Default donation_amount_second. Donation amount button that's selected by default.", + "default": "donation_amount_second" + }, + "icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "icon_dark_theme": { + "type": "string", + "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred." + }, + "icon_alt_text": { + "type": "string", + "description": "Alt text for accessibility", + "default": "" + }, + "title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { "description": "Snippet title displayed before snippet text" } + ] + }, + "title_icon": { + "type": "string", + "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale." + }, + "title_icon_dark_theme": { + "type": "string", + "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale." + }, + "button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ] + }, + "button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "block_button_text": { + "type": "string", + "description": "Tooltip text used for dismiss button." + }, + "monthly_checkbox_label_text": { + "type": "string", + "description": "Label text for monthly checkbox.", + "default": "Make my donation monthly" + }, + "test": { + "type": "string", + "description": "Different styles for the snippet. Options are bold and takeover." + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked" + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + }, + "args": { + "type": "string", + "description": "Additional parameters for link action, example which specific menu the button should open" + } + } + } + }, + "additionalProperties": false, + "required": [ + "text", + "donation_form_url", + "donation_amount_first", + "donation_amount_second", + "donation_amount_third", + "donation_amount_fourth", + "button_label", + "currency_code" + ], + "dependencies": { + "button_color": ["button_label"], + "button_background_color": ["button_label"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/_EOYSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/_EOYSnippet.scss new file mode 100644 index 0000000000..d9911ff02c --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/_EOYSnippet.scss @@ -0,0 +1,55 @@ +.EOYSnippetForm { + margin: 10px 0 8px; + align-self: start; + font-size: 14px; + display: flex; + align-items: center; + + .donation-amount, + .donation-form-url { + white-space: nowrap; + font-size: 14px; + padding: 8px 20px; + border-radius: 2px; + } + + .donation-amount { + color: var(--newtab-text-primary-color); + margin-inline-end: 18px; + border: $input-border; + padding: 5px 14px; + background: var(--newtab-background-color-secondary); + cursor: pointer; + } + + input { + &[type='radio'] { + opacity: 0; + margin-inline-end: -18px; + + &:checked + .donation-amount { + // Use a text color for the background to achieve an inverted look. + background: var(--newtab-text-secondary-color); + color: var(--newtab-background-color-secondary); + border: $border-secondary; + } + + // accessibility + &:checked:focus + .donation-amount, + &:not(:checked):focus + .donation-amount { + border: 1px dotted var(--newtab-primary-action-background); + } + } + } + + .monthly-checkbox-container { + display: flex; + width: 100%; + } + + .donation-form-url { + margin-inline-start: 18px; + align-self: flex-end; + display: flex; + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx new file mode 100644 index 0000000000..1d8197d675 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx"; + +export const FXASignupSnippet = props => { + const userAgent = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./); + const firefox_version = userAgent ? parseInt(userAgent[1], 10) : 0; + const extendedContent = { + scene1_button_label: "Learn more", + retry_button_label: "Try again", + scene2_email_placeholder_text: "Your email here", + scene2_button_label: "Sign me up", + scene2_dismiss_button_text: "Dismiss", + ...props.content, + hidden_inputs: { + action: "email", + context: "fx_desktop_v3", + entrypoint: "snippets", + utm_source: "snippet", + utm_content: firefox_version, + utm_campaign: props.content.utm_campaign, + utm_term: props.content.utm_term, + ...props.content.hidden_inputs, + }, + }; + + return ( + + ); +}; diff --git a/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json new file mode 100644 index 0000000000..315aaba7a0 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json @@ -0,0 +1,196 @@ +{ + "title": "FXASignupSnippet", + "description": "A snippet template for FxA sign up/sign in", + "version": "1.2.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "scene1_title": { + "allof": [ + { "$ref": "#/definitions/plainText" }, + { "description": "snippet title displayed before snippet text" } + ] + }, + "scene1_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_section_title_icon": { + "type": "string", + "description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_icon_dark_theme": { + "type": "string", + "description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_text": { + "type": "string", + "description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display." + }, + "scene1_section_title_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, scene1_section_title_text links to this" } + ] + }, + "scene2_title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Title displayed before text in scene 2. Should be plain text." + } + ] + }, + "scene2_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "scene1_icon_dark_theme": { + "type": "string", + "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred." + }, + "scene1_title_icon": { + "type": "string", + "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene1_title_icon_dark_theme": { + "type": "string", + "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene2_email_placeholder_text": { + "type": "string", + "description": "Value to show while input is empty.", + "default": "Your email here" + }, + "scene2_button_label": { + "type": "string", + "description": "Label for form submit button", + "default": "Sign me up" + }, + "scene2_dismiss_button_text": { + "type": "string", + "description": "Label for the dismiss button when the sign-up form is expanded.", + "default": "Dismiss" + }, + "hidden_inputs": { + "type": "object", + "description": "Each entry represents a hidden input, key is used as value for the name property.", + "properties": { + "action": { + "type": "string", + "enum": ["email"] + }, + "context": { + "type": "string", + "enum": ["fx_desktop_v3"] + }, + "entrypoint": { + "type": "string", + "enum": ["snippets"] + }, + "utm_content": { + "type": "number", + "description": "Firefox version number" + }, + "utm_source": { + "type": "string", + "enum": ["snippet"] + }, + "utm_campaign": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_campaign." + }, + "utm_term": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_term." + }, + "additionalProperties": false + } + }, + "scene1_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ], + "default": "Learn more" + }, + "scene1_button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "scene1_button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "retry_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for the button in the event of a submission error/failure." + } + ], + "default": "Try again" + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked", + "default": false + }, + "utm_campaign": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_campaign." + }, + "utm_term": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_term." + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + } + } + } + }, + "additionalProperties": false, + "required": ["scene1_text", "scene2_text", "scene1_button_label"], + "dependencies": { + "scene1_button_color": ["scene1_button_label"], + "scene1_button_background_color": ["scene1_button_label"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js b/browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js new file mode 100644 index 0000000000..6fc4d2283a --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * BASE_PARAMS keys/values can be modified from outside this file + */ +export const BASE_PARAMS = { + utm_source: "activity-stream", + utm_campaign: "firstrun", + utm_medium: "referral", +}; + +/** + * Takes in a url as a string or URL object and returns a URL object with the + * utm_* parameters added to it. If a URL object is passed in, the paraemeters + * are added to it (the return value can be ignored in that case as it's the + * same object). + */ +export function addUtmParams(url, utmTerm) { + let returnUrl = url; + if (typeof returnUrl === "string") { + returnUrl = new URL(url); + } + for (let [key, value] of Object.entries(BASE_PARAMS)) { + if (!returnUrl.searchParams.has(key)) { + returnUrl.searchParams.append(key, value); + } + } + returnUrl.searchParams.append("utm_term", utmTerm); + return returnUrl; +} diff --git a/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx new file mode 100644 index 0000000000..27c1684762 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx"; + +export const NewsletterSnippet = props => { + const extendedContent = { + scene1_button_label: "Learn more", + retry_button_label: "Try again", + scene2_email_placeholder_text: "Your email here", + scene2_button_label: "Sign me up", + scene2_dismiss_button_text: "Dismiss", + scene2_newsletter: "mozilla-foundation", + ...props.content, + hidden_inputs: { + newsletters: props.content.scene2_newsletter || "mozilla-foundation", + fmt: "H", + lang: props.content.locale || "en-US", + source_url: `https://snippets.mozilla.com/show/${props.id}`, + ...props.content.hidden_inputs, + }, + }; + + return ( + + ); +}; diff --git a/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json new file mode 100644 index 0000000000..c77261c191 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json @@ -0,0 +1,186 @@ +{ + "title": "NewsletterSnippet", + "description": "A snippet template for send to device mobile download", + "version": "1.2.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "locale": { + "type": "string", + "description": "Two to five character string for the locale code", + "default": "en-US" + }, + "scene1_title": { + "allof": [ + { "$ref": "#/definitions/plainText" }, + { "description": "snippet title displayed before snippet text" } + ] + }, + "scene1_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_section_title_icon": { + "type": "string", + "description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_icon_dark_theme": { + "type": "string", + "description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_text": { + "type": "string", + "description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display." + }, + "scene1_section_title_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, scene1_section_title_text links to this" } + ] + }, + "scene2_title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Title displayed before text in scene 2. Should be plain text." + } + ] + }, + "scene2_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "scene1_icon_dark_theme": { + "type": "string", + "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred." + }, + "scene1_title_icon": { + "type": "string", + "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene1_title_icon_dark_theme": { + "type": "string", + "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene2_email_placeholder_text": { + "type": "string", + "description": "Value to show while input is empty.", + "default": "Your email here" + }, + "scene2_button_label": { + "type": "string", + "description": "Label for form submit button", + "default": "Sign me up" + }, + "scene2_privacy_html": { + "type": "string", + "description": "(send to device) Html for disclaimer and link underneath input box." + }, + "scene2_dismiss_button_text": { + "type": "string", + "description": "Label for the dismiss button when the sign-up form is expanded.", + "default": "Dismiss" + }, + "hidden_inputs": { + "type": "object", + "description": "Each entry represents a hidden input, key is used as value for the name property.", + "properties": { + "fmt": { + "type": "string", + "description": "", + "default": "H" + } + } + }, + "scene1_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ], + "default": "Learn more" + }, + "scene1_button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "scene1_button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "retry_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for the button in the event of a submission error/failure." + } + ], + "default": "Try again" + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked", + "default": false + }, + "success_text": { + "type": "string", + "description": "Message shown on successful registration." + }, + "error_text": { + "type": "string", + "description": "Message shown if registration failed." + }, + "scene2_newsletter": { + "type": "string", + "description": "Newsletter/basket id user is subscribing to.", + "default": "mozilla-foundation" + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + } + } + } + }, + "additionalProperties": false, + "required": ["scene1_text", "scene2_text", "scene1_button_label"], + "dependencies": { + "scene1_button_color": ["scene1_button_label"], + "scene1_button_background_color": ["scene1_button_label"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ProtectionsPanelMessage.schema.json b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ProtectionsPanelMessage.schema.json new file mode 100644 index 0000000000..8ef9b802e1 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ProtectionsPanelMessage.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///ProtectionsPanelMessage.schema.json", + "title": "ProtectionsPanelMessage", + "description": "A message shown in the protections panel.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "title": { + "description": "The message title.", + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText" + }, + "body": { + "description": "The body of the message.", + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText" + }, + "link_text": { + "description": "The text of the call to action link.", + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText" + }, + "cta_type": { + "description": "The type of URL open action.", + "type": "string", + "enum": ["OPEN_URL", "OPEN_PROTECTION_REPORT", "OPEN_ABOUT_PAGE"] + }, + "cta_url": { + "description": "The URL to open when the call to action is clicked", + "type": "string", + "format": "moz-url-format" + }, + "cta_where": { + "description": "How to open the cta.", + "type": "string", + "enum": ["current", "tabshifted", "tab", "save", "window"] + } + }, + "dependantSchemas": { + "link_text": ["cta_type", "cta_url"], + "cta_type": ["link_text"], + "cta_url": ["link_text"], + "cta_where": ["link_text"] + }, + "additionalProperties": false, + "required": ["title", "body"] + }, + "template": { + "type": "string", + "const": "protections_panel" + }, + "trigger": { + "description": "An action to trigger potentially showing the message. The action ID `protectionsPanelOpen` is required.", + "const": { + "id": "protectionsPanelOpen" + } + } + }, + "required": ["content", "template", "trigger"], + "additionalProperties": true +} diff --git a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json new file mode 100644 index 0000000000..5d5b98f594 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///Spotlight.schema.json", + "title": "Spotlight", + "description": "A template with an image, title, content and two buttons.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "Specify the layout template for the Spotlight", + "const": "multistage" + }, + "backdrop": { + "type": "string", + "description": "Background css behind modal content" + }, + "logo": { + "type": "object", + "properties": { + "imageURL": { + "type": "string", + "description": "URL for image to use with the content" + }, + "imageId": { + "type": "string", + "description": "The ID for a remotely hosted image" + }, + "size": { + "type": "string", + "description": "The logo size." + } + }, + "additionalProperties": true + }, + "screens": { + "type": "array", + "description": "Collection of individual screen content" + }, + "transitions": { + "type": "boolean", + "description": "Show transitions within and between screens" + }, + "disableHistoryUpdates": { + "type": "boolean", + "description": "Don't alter the browser session's history stack - used with messaging surfaces like Feature Callouts" + }, + "startScreen": { + "type": "integer", + "description": "Index of first screen to show from message, defaulting to 0" + } + }, + "additionalProperties": true + }, + "template": { + "type": "string", + "description": "Specify whether the surface is shown as a Spotlight modal or an in-surface Feature Callout dialog", + "enum": ["spotlight", "feature_callout"] + } + }, + "additionalProperties": true, + "required": ["targeting"] +} diff --git a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json new file mode 100644 index 0000000000..4ec7dc9522 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///ToolbarBadgeMessage.schema.json", + "title": "ToolbarBadgeMessage", + "description": "A template that specifies to which element in the browser toolbar to add a notification.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "target": { + "type": "string" + }, + "action": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "additionalProperties": true, + "required": ["id"], + "description": "Optional action to take in addition to showing the notification" + }, + "delay": { + "type": "number", + "description": "Optional delay in ms after which to show the notification" + }, + "badgeDescription": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizedText", + "description": "This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'" + } + }, + "additionalProperties": true, + "required": ["target"] + }, + "template": { + "type": "string", + "const": "toolbar_badge" + } + }, + "additionalProperties": true, + "required": ["targeting"] +} diff --git a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json new file mode 100644 index 0000000000..c5a466a6e5 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///UpdateAction.schema.json", + "title": "UpdateActionMessage", + "description": "A template for messages that execute predetermined actions.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "action": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "data": { + "type": "object", + "description": "Additional data provided as argument when executing the action", + "properties": { + "url": { + "type": "string", + "description": "URL data to be used as argument to the action" + }, + "expireDelta": { + "type": "number", + "description": "Expiration timestamp to be used as argument to the action" + } + } + } + }, + "additionalProperties": true, + "description": "Optional action to take in addition to showing the notification", + "required": ["id", "data"] + } + }, + "additionalProperties": true, + "required": ["action"] + }, + "template": { + "type": "string", + "const": "update_action" + } + }, + "required": ["targeting"] +} diff --git a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json new file mode 100644 index 0000000000..26e795d068 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///WhatsNewMessage.schema.json", + "title": "WhatsNewMessage", + "description": "A template for the messages that appear in the What's New panel.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "layout": { + "description": "Different message layouts", + "enum": ["tracking-protections"] + }, + "bucket_id": { + "type": "string", + "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting." + }, + "published_date": { + "type": "integer", + "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published." + }, + "title": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "Id of localized string or message override of What's New message title" + }, + "subtitle": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "Id of localized string or message override of What's New message subtitle" + }, + "body": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "Id of localized string or message override of What's New message body" + }, + "link_text": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "(optional) Id of localized string or message override of What's New message link text" + }, + "cta_url": { + "description": "Target URL for the What's New message.", + "type": "string", + "format": "moz-url-format" + }, + "cta_type": { + "description": "Type of url open action", + "enum": ["OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PROTECTION_REPORT"] + }, + "cta_where": { + "description": "How to open the cta: new window, tab, focused, unfocused.", + "enum": ["current", "tabshifted", "tab", "save", "window"] + }, + "icon_url": { + "description": "(optional) URL for the What's New message icon.", + "type": "string", + "format": "uri" + }, + "icon_alt": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "Alt text for image." + } + }, + "additionalProperties": true, + "required": ["published_date", "title", "body", "cta_url", "bucket_id"] + }, + "template": { + "type": "string", + "const": "whatsnew_panel_message" + } + }, + "required": ["order"], + "additionalProperties": true +} diff --git a/browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json b/browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json new file mode 100644 index 0000000000..3719419428 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json @@ -0,0 +1,153 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///NewtabPromoMessage.schema.json", + "title": "PBNewtabPromoMessage", + "description": "Message shown on the private browsing newtab page.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "hideDefault": { + "type": "boolean", + "description": "Should we hide the default promo after the experiment promo is dismissed." + }, + "infoEnabled": { + "type": "boolean", + "description": "Should we show the info section." + }, + "infoIcon": { + "type": "string", + "description": "Icon shown in the left side of the info section. Default is the private browsing icon." + }, + "infoTitle": { + "type": "string", + "description": "Is the title in the info section enabled." + }, + "infoTitleEnabled": { + "type": "boolean", + "description": "Is the title in the info section enabled." + }, + "infoBody": { + "type": "string", + "description": "Text content in the info section." + }, + "infoLinkText": { + "type": "string", + "description": "Text for the link in the info section." + }, + "infoLinkUrl": { + "type": "string", + "description": "URL for the info section link.", + "format": "moz-url-format" + }, + "promoEnabled": { + "type": "boolean", + "description": "Should we show the promo section." + }, + "promoType": { + "type": "string", + "description": "Promo type used to determine if promo should show to a given user", + "enum": ["FOCUS", "VPN", "PIN", "COOKIE_BANNERS", "OTHER"] + }, + "promoSectionStyle": { + "type": "string", + "description": "Sets the position of the promo section. Possible values are: top, below-search, bottom. Default bottom.", + "enum": ["top", "below-search", "bottom"] + }, + "promoTitle": { + "type": "string", + "description": "The text content of the promo section." + }, + "promoTitleEnabled": { + "type": "boolean", + "description": "Should we show text content in the promo section." + }, + "promoLinkText": { + "type": "string", + "description": "The text of the link in the promo box." + }, + "promoHeader": { + "type": "string", + "description": "The title of the promo section." + }, + "promoButton": { + "type": "object", + "properties": { + "action": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Action dispatched by the button." + }, + "data": { + "type": "object" + } + }, + "required": ["type"], + "additionalProperties": true + } + }, + "required": ["action"] + }, + "promoLinkType": { + "type": "string", + "description": "Type of promo link type. Possible values: link, button. Default is link.", + "enum": ["link", "button"] + }, + "promoImageLarge": { + "type": "string", + "description": "URL for image used on the left side of the promo box, larger, showcases some feature. Default off.", + "format": "uri" + }, + "promoImageSmall": { + "type": "string", + "description": "URL for image used on the right side of the promo box, smaller, usually a logo. Default off.", + "format": "uri" + } + }, + "additionalProperties": true, + "allOf": [ + { + "if": { + "properties": { + "promoEnabled": { "const": true } + }, + "required": ["promoEnabled"] + }, + "then": { + "required": ["promoButton"] + } + }, + { + "if": { + "properties": { + "infoEnabled": { "const": true } + }, + "required": ["infoEnabled"] + }, + "then": { + "required": ["infoLinkText"], + "if": { + "properties": { + "infoTitleEnabled": { "const": true } + }, + "required": ["infoTitleEnabled"] + }, + "then": { + "required": ["infoTitle"] + } + } + } + ] + }, + "template": { + "type": "string", + "const": "pb_newtab" + } + }, + "additionalProperties": true, + "required": ["targeting"] +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx new file mode 100644 index 0000000000..0929b8f711 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { isEmailOrPhoneNumber } from "./isEmailOrPhoneNumber"; +import React from "react"; +import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx"; + +function validateInput(value, content) { + const type = isEmailOrPhoneNumber(value, content); + return type ? "" : "Must be an email or a phone number."; +} + +function processFormData(input, message) { + const { content } = message; + const type = content.include_sms + ? isEmailOrPhoneNumber(input.value, content) + : "email"; + const formData = new FormData(); + let url; + if (type === "phone") { + url = "https://basket.mozilla.org/news/subscribe_sms/"; + formData.append("mobile_number", input.value); + formData.append("msg_name", content.message_id_sms); + formData.append("country", content.country); + } else if (type === "email") { + url = "https://basket.mozilla.org/news/subscribe/"; + formData.append("email", input.value); + formData.append("newsletters", content.message_id_email); + formData.append( + "source_url", + encodeURIComponent(`https://snippets.mozilla.com/show/${message.id}`) + ); + } + formData.append("lang", content.locale); + return { formData, url }; +} + +function addDefaultValues(props) { + return { + ...props, + content: { + scene1_button_label: "Learn more", + retry_button_label: "Try again", + scene2_dismiss_button_text: "Dismiss", + scene2_button_label: "Send", + scene2_input_placeholder: "Your email here", + locale: "en-US", + country: "us", + message_id_email: "", + include_sms: false, + ...props.content, + }, + }; +} + +export const SendToDeviceSnippet = props => { + const propsWithDefaults = addDefaultValues(props); + + return ( + + ); +}; + +export const SendToDeviceScene2Snippet = props => { + return ; +}; diff --git a/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json new file mode 100644 index 0000000000..34567443f4 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json @@ -0,0 +1,243 @@ +{ + "title": "SendToDeviceSnippet", + "description": "A snippet template for send to device mobile download", + "version": "1.2.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "locale": { + "type": "string", + "description": "Two to five character string for the locale code", + "default": "en-US" + }, + "country": { + "type": "string", + "description": "Two character string for the country code (used for SMS)", + "default": "us" + }, + "scene1_title": { + "allof": [ + { "$ref": "#/definitions/plainText" }, + { "description": "snippet title displayed before snippet text" } + ] + }, + "scene1_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_section_title_icon": { + "type": "string", + "description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_icon_dark_theme": { + "type": "string", + "description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_text": { + "type": "string", + "description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display." + }, + "scene1_section_title_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, scene1_section_title_text links to this" } + ] + }, + "scene2_title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Title displayed before text in scene 2. Should be plain text." + } + ] + }, + "scene2_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "scene1_icon_dark_theme": { + "type": "string", + "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred." + }, + "scene2_icon": { + "type": "string", + "description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred." + }, + "scene2_icon_dark_theme": { + "type": "string", + "description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred." + }, + "scene1_title_icon": { + "type": "string", + "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene1_title_icon_dark_theme": { + "type": "string", + "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene2_button_label": { + "type": "string", + "description": "Label for form submit button", + "default": "Send" + }, + "scene2_input_placeholder": { + "type": "string", + "description": "(send to device) Value to show while input is empty.", + "default": "Your email here" + }, + "scene2_disclaimer_html": { + "type": "string", + "description": "(send to device) Html for disclaimer and link underneath input box." + }, + "scene2_dismiss_button_text": { + "type": "string", + "description": "Label for the dismiss button when the sign-up form is expanded.", + "default": "Dismiss" + }, + "hidden_inputs": { + "type": "object", + "description": "Each entry represents a hidden input, key is used as value for the name property.", + "properties": { + "action": { + "type": "string", + "enum": ["email"] + }, + "context": { + "type": "string", + "enum": ["fx_desktop_v3"] + }, + "entrypoint": { + "type": "string", + "enum": ["snippets"] + }, + "utm_content": { + "type": "string", + "description": "Firefox version number" + }, + "utm_source": { + "type": "string", + "enum": ["snippet"] + }, + "utm_campaign": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_campaign." + }, + "utm_term": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_term." + }, + "additionalProperties": false + } + }, + "scene1_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ], + "default": "Learn more" + }, + "scene1_button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "scene1_button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "retry_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for the button in the event of a submission error/failure." + } + ], + "default": "Try again" + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked", + "default": false + }, + "success_title": { + "type": "string", + "description": "(send to device) Title shown before text on successful registration." + }, + "success_text": { + "type": "string", + "description": "Message shown on successful registration." + }, + "error_text": { + "type": "string", + "description": "Message shown if registration failed." + }, + "include_sms": { + "type": "boolean", + "description": "(send to device) Allow users to send an SMS message with the form?", + "default": false + }, + "message_id_sms": { + "type": "string", + "description": "(send to device) Newsletter/basket id representing the SMS message to be sent." + }, + "message_id_email": { + "type": "string", + "description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/." + }, + "utm_campaign": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_campaign." + }, + "utm_term": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_term." + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + } + } + } + }, + "additionalProperties": false, + "required": ["scene1_text", "scene2_text", "scene1_button_label"], + "dependencies": { + "scene1_button_color": ["scene1_button_label"], + "scene1_button_background_color": ["scene1_button_label"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js new file mode 100644 index 0000000000..44ef622227 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Checks if a given string is an email or phone number or neither + * @param {string} val The user input + * @param {ASRMessageContent} content .content property on ASR message + * @returns {"email"|"phone"|""} The type of the input + */ +export function isEmailOrPhoneNumber(val, content) { + const { locale } = content; + // http://emailregex.com/ + const email_re = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const check_email = email_re.test(val); + let check_phone; // depends on locale + switch (locale) { + case "en-US": + case "en-CA": + // allow 10-11 digits in case user wants to enter country code + check_phone = val.length >= 10 && val.length <= 11 && !isNaN(val); + break; + case "de": + // allow between 2 and 12 digits for german phone numbers + check_phone = val.length >= 2 && val.length <= 12 && !isNaN(val); + break; + // this case should never be hit, but good to have a fallback just in case + default: + check_phone = !isNaN(val); + break; + } + if (check_email) { + return "email"; + } else if (check_phone) { + return "phone"; + } + return ""; +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx new file mode 100644 index 0000000000..2641d51e86 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { Button } from "../../components/Button/Button"; +import { RichText } from "../../components/RichText/RichText"; +import { safeURI } from "../../template-utils"; +import { SnippetBase } from "../../components/SnippetBase/SnippetBase"; + +const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; +// Alt text placeholder in case the prop from the server isn't available +const ICON_ALT_TEXT = ""; + +export class SimpleBelowSearchSnippet extends React.PureComponent { + constructor(props) { + super(props); + this.onButtonClick = this.onButtonClick.bind(this); + } + + renderText() { + const { props } = this; + return props.content.text ? ( + + ) : null; + } + + renderTitle() { + const { title } = this.props.content; + return title ? ( +

+ {title} +
+

+ ) : null; + } + + async onButtonClick() { + if (this.props.provider !== "preview") { + this.props.sendUserActionTelemetry({ + event: "CLICK_BUTTON", + id: this.props.UISurface, + }); + } + const { button_url } = this.props.content; + // If button_url is defined handle it as OPEN_URL action + const type = this.props.content.button_action || (button_url && "OPEN_URL"); + await this.props.onAction({ + type, + data: { args: this.props.content.button_action_args || button_url }, + }); + if (!this.props.content.do_not_autoblock) { + this.props.onBlock(); + } + } + + _shouldRenderButton() { + return ( + this.props.content.button_action || + this.props.onButtonClick || + this.props.content.button_url + ); + } + + renderButton() { + const { props } = this; + if (!this._shouldRenderButton()) { + return null; + } + + return ( + + ); + } + + render() { + const { props } = this; + let className = "SimpleBelowSearchSnippet"; + let containerName = "below-search-snippet"; + + if (props.className) { + className += ` ${props.className}`; + } + if (this._shouldRenderButton()) { + className += " withButton"; + containerName += " withButton"; + } + + return ( +
+
+ + {props.content.icon_alt_text + {props.content.icon_alt_text +
+ {this.renderTitle()} +

{this.renderText()}

+ {this.props.extraContent} +
+ {
{this.renderButton()}
} +
+
+
+ ); + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json new file mode 100644 index 0000000000..06368257f0 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json @@ -0,0 +1,114 @@ +{ + "title": "SimpleBelowSearchSnippet", + "description": "A simple template with an icon, rich text and an optional button. It gets inserted below the Activity Stream search box.", + "version": "1.2.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { "description": "Snippet title displayed before snippet text" } + ] + }, + "text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "icon_dark_theme": { + "type": "string", + "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred." + }, + "icon_alt_text": { + "type": "string", + "description": "Alt text describing icon for screen readers", + "default": "" + }, + "block_button_text": { + "type": "string", + "description": "Tooltip text used for dismiss button.", + "default": "Remove this" + }, + "button_action": { + "type": "string", + "description": "The type of action the button should trigger." + }, + "button_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, button_label links to this" } + ] + }, + "button_action_args": { + "description": "Additional parameters for button action, example which specific menu the button should open" + }, + "button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ] + }, + "button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA link has been clicked" + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + }, + "args": { + "type": "string", + "description": "Additional parameters for link action, example which specific menu the button should open" + } + } + } + }, + "additionalProperties": false, + "required": ["text"], + "dependencies": { + "button_action": ["button_label"], + "button_url": ["button_label"], + "button_color": ["button_label"], + "button_background_color": ["button_label"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss new file mode 100644 index 0000000000..9d902b4cbb --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss @@ -0,0 +1,190 @@ + +.below-search-snippet { + margin: 0 auto 16px; + + &.withButton { + margin: auto; + min-height: 60px; + background-color: transparent; + + .snippet-hover-wrapper { + min-height: 60px; + border-radius: 4px; + + &:hover { + background-color: var(--newtab-element-hover-color); + + .blockButton { + display: block; + opacity: 1; + } + } + } + } +} + +.SimpleBelowSearchSnippet { + background-color: transparent; + border: 0; + box-shadow: none; + position: relative; + margin: auto; + z-index: auto; + + @media (min-width: $break-point-large) { + width: 736px; + } + + &.active { + background-color: var(--newtab-element-hover-color); + border-radius: 4px; + } + + .innerWrapper { + align-items: center; + background-color: transparent; + border-radius: 4px; + box-shadow: $shadow-card; + flex-direction: column; + padding: 16px; + text-align: center; + width: 100%; + + @media (min-width: $break-point-medium) { + align-items: flex-start; + background-color: transparent; + border-radius: 4px; + box-shadow: none; + flex-direction: row; + padding: 0; + text-align: inherit; + width: 696px; + } + + @media (max-width: 865px) { + margin-inline-start: 0; + } + + // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 610px. + @media (max-width: $break-point-medium - 1px) { + margin: auto; + } + } + + .blockButton { + display: block; + inset-inline-end: 10px; + opacity: 1; + top: 50%; + + &:focus { + box-shadow: $shadow-primary; + border-radius: 2px; + } + } + + .title { + font-size: inherit; + margin: 0; + } + + .title-inline { + display: inline; + } + + .textContainer { + margin: 10px; + margin-inline-start: 0; + padding-inline-end: 20px; + } + + .icon { + margin-top: 8px; + margin-inline-start: 12px; + height: 32px; + width: 32px; + + @media (min-width: $break-point-medium) { + height: 24px; + width: 24px; + } + + @media (max-width: $break-point-medium) { + margin: auto; + } + } + + &.withButton { + line-height: 20px; + margin-bottom: 10px; + min-height: 60px; + background-color: transparent; + + .innerWrapper { + // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px. + @media (max-width: $break-point-widest + 1px) { + margin: 0 40px; + } + } + + .blockButton { + display: block; + inset-inline-end: -10%; + opacity: 0; + margin: auto; + top: unset; + + &:focus { + opacity: 1; + box-shadow: none; + } + + // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px. + @media (max-width: $break-point-widest + 1px) { + inset-inline-end: 2%; + } + } + + .icon { + width: 42px; + height: 42px; + flex-shrink: 0; + margin: auto 0; + margin-inline-end: 10px; + + @media (max-width: $break-point-medium) { + margin: auto; + } + } + + .buttonContainer { + margin: auto; + margin-inline-end: 0; + + @media (max-width: $break-point-medium) { + margin: auto; + } + } + } + + button { + @media (max-width: $break-point-medium) { + margin: auto; + } + } + + .body { + display: inline; + position: sticky; + transform: translateY(-50%); + margin: 8px 0 0; + + @media (min-width: $break-point-medium) { + margin: 12px 0; + } + + a { + font-weight: 600; + } + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx new file mode 100644 index 0000000000..96570e2dbd --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx @@ -0,0 +1,222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Button } from "../../components/Button/Button"; +import ConditionalWrapper from "../../components/ConditionalWrapper/ConditionalWrapper"; +import React from "react"; +import { RichText } from "../../components/RichText/RichText"; +import { safeURI } from "../../template-utils"; +import { SnippetBase } from "../../components/SnippetBase/SnippetBase"; + +const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; +// Alt text placeholder in case the prop from the server isn't available +const ICON_ALT_TEXT = ""; + +export class SimpleSnippet extends React.PureComponent { + constructor(props) { + super(props); + this.onButtonClick = this.onButtonClick.bind(this); + } + + onButtonClick() { + if (this.props.provider !== "preview") { + this.props.sendUserActionTelemetry({ + event: "CLICK_BUTTON", + id: this.props.UISurface, + }); + } + const { button_url, button_entrypoint_value, button_entrypoint_name } = + this.props.content; + // If button_url is defined handle it as OPEN_URL action + const type = this.props.content.button_action || (button_url && "OPEN_URL"); + // Assign the snippet referral for the action + const entrypoint = button_entrypoint_name + ? new URLSearchParams([ + [button_entrypoint_name, button_entrypoint_value], + ]).toString() + : button_entrypoint_value; + this.props.onAction({ + type, + data: { + args: this.props.content.button_action_args || button_url, + ...(entrypoint && { entrypoint }), + }, + }); + if (!this.props.content.do_not_autoblock) { + this.props.onBlock(); + } + } + + _shouldRenderButton() { + return ( + this.props.content.button_action || + this.props.onButtonClick || + this.props.content.button_url + ); + } + + renderTitle() { + const { title } = this.props.content; + return title ? ( +

+ {this.renderTitleIcon()} {title} +

+ ) : null; + } + + renderTitleIcon() { + const titleIconLight = safeURI(this.props.content.title_icon); + const titleIconDark = safeURI( + this.props.content.title_icon_dark_theme || this.props.content.title_icon + ); + if (!titleIconLight) { + return null; + } + + return ( + + + + + ); + } + + renderButton() { + const { props } = this; + if (!this._shouldRenderButton()) { + return null; + } + + return ( + + ); + } + + renderText() { + const { props } = this; + return ( + + ); + } + + wrapSectionHeader(url) { + return function (children) { + return
{children}; + }; + } + + wrapSnippetContent(children) { + return
{children}
; + } + + renderSectionHeader() { + const { props } = this; + + // an icon and text must be specified to render the section header + if (props.content.section_title_icon && props.content.section_title_text) { + const sectionTitleIconLight = safeURI(props.content.section_title_icon); + const sectionTitleIconDark = safeURI( + props.content.section_title_icon_dark_theme || + props.content.section_title_icon + ); + const sectionTitleURL = props.content.section_title_url; + + return ( +
+

+ + + + + {props.content.section_title_text} + + +

+
+ ); + } + + return null; + } + + render() { + const { props } = this; + const sectionHeader = this.renderSectionHeader(); + let className = "SimpleSnippet"; + + if (props.className) { + className += ` ${props.className}`; + } + if (props.content.tall) { + className += " tall"; + } + if (sectionHeader) { + className += " has-section-header"; + } + + return ( +
+ + {sectionHeader} + + {props.content.icon_alt_text + {props.content.icon_alt_text +
+ {this.renderTitle()}

{this.renderText()}

+ {this.props.extraContent} +
+ {
{this.renderButton()}
} +
+
+
+ ); + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json new file mode 100644 index 0000000000..4970b124af --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json @@ -0,0 +1,159 @@ +{ + "title": "SimpleSnippet", + "description": "A simple template with an icon, text, and optional button.", + "version": "1.1.2", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { "description": "Snippet title displayed before snippet text" } + ] + }, + "text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "icon_dark_theme": { + "type": "string", + "description": "Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred." + }, + "icon_alt_text": { + "type": "string", + "description": "Alt text describing icon for screen readers", + "default": "" + }, + "title_icon": { + "type": "string", + "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale." + }, + "title_icon_dark_theme": { + "type": "string", + "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale." + }, + "title_icon_alt_text": { + "type": "string", + "description": "Alt text describing title icon for screen readers", + "default": "" + }, + "button_action": { + "type": "string", + "description": "The type of action the button should trigger." + }, + "button_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, button_label links to this" } + ] + }, + "button_action_args": { + "description": "Additional parameters for button action, example which specific menu the button should open" + }, + "button_entrypoint_value": { + "description": "String used for telemetry attribution of clicks", + "type": "string" + }, + "button_entrypoint_name": { + "description": "String used for telemetry attribution of clicks", + "type": "string" + }, + "button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ] + }, + "button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "block_button_text": { + "type": "string", + "description": "Tooltip text used for dismiss button.", + "default": "Remove this" + }, + "tall": { + "type": "boolean", + "description": "To be used by fundraising only, increases height to roughly 120px. Defaults to false." + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked" + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + }, + "args": { + "type": "string", + "description": "Additional parameters for link action, example which specific menu the button should open" + } + } + }, + "section_title_icon": { + "type": "string", + "description": "Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display." + }, + "section_title_icon_dark_theme": { + "type": "string", + "description": "Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display." + }, + "section_title_text": { + "type": "string", + "description": "Section title text. section_title_icon must also be specified to display." + }, + "section_title_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, section_title_text links to this" } + ] + } + }, + "additionalProperties": false, + "required": ["text"], + "dependencies": { + "button_action": ["button_label"], + "button_url": ["button_label"], + "button_color": ["button_label"], + "button_background_color": ["button_label"], + "section_title_url": ["section_title_text"], + "button_entrypoint_name": ["button_entrypoint_value"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss new file mode 100644 index 0000000000..1ee83a5cc9 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss @@ -0,0 +1,131 @@ +$section-header-height: 30px; +$icon-width: 54px; // width of primary icon + margin + +.SimpleSnippet { + &.tall { + padding: 27px 0; + } + + p em { + color: var(--newtab-text-emphasis-text-color); + font-style: normal; + background: var(--newtab-text-emphasis-background); + } + + &.bold { + height: 176px; + + .body { + font-size: 14px; + line-height: 20px; + margin-bottom: 20px; + } + + .icon { + width: 71px; + height: 71px; + } + } + + &.takeover { + height: 344px; + + .body { + font-size: 16px; + line-height: 24px; + margin-bottom: 35px; + } + + .icon { + width: 79px; + height: 79px; + } + } + + .title { + font-size: inherit; + margin: 0; + } + + .title-inline { + display: inline; + } + + .titleIcon { + background-repeat: no-repeat; + background-size: 14px; + background-position: center; + height: 16px; + width: 16px; + margin-top: 2px; + margin-inline-end: 2px; + display: inline-block; + vertical-align: top; + } + + .body { + display: inline; + margin: 0; + } + + &.tall .icon { + margin-inline-end: 20px; + } + + &.bold, + &.takeover { + .donation-form-url, + .donation-amount { + padding-block: 8px; + } + + .icon { + margin-inline-end: 20px; + } + } + + .icon { + align-self: flex-start; + } + + &.has-section-header .innerWrapper { + // account for section header being 100% width + flex-wrap: wrap; + padding-top: 7px; + } + + // wrapper div added if section-header is displayed that allows icon/text/button + // to squish instead of wrapping. this is effectively replicating layout behavior + // when section-header is *not* present. + .innerContentWrapper { + align-items: center; + display: flex; + } + + .section-header { + flex: 0 0 100%; + margin-bottom: 10px; + } + + .section-title { + // color should match that of 'Recommended by Pocket' and 'Highlights' in newtab page + color: var(--newtab-text-primary-color); + display: inline-block; + font-size: 13px; + font-weight: bold; + margin: 0; + + a { + color: var(--newtab-text-primary-color); + font-weight: inherit; + text-decoration: none; + } + + .icon { + height: 16px; + margin-inline-end: 6px; + margin-top: -2px; + width: 16px; + } + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormScene2Snippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormScene2Snippet.schema.json new file mode 100644 index 0000000000..12eeecc084 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormScene2Snippet.schema.json @@ -0,0 +1,167 @@ +{ + "title": "SubmitFormSnippet", + "description": "A template with two states: a SimpleSnippet and another that contains a form", + "version": "1.2.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "locale": { + "type": "string", + "description": "Two to five character string for the locale code" + }, + "country": { + "type": "string", + "description": "Two character string for the country code (used for SMS)" + }, + "section_title_icon": { + "type": "string", + "description": "Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display." + }, + "section_title_icon_dark_theme": { + "type": "string", + "description": "Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display." + }, + "section_title_text": { + "type": "string", + "description": "Section title text. section_title_icon must also be specified to display." + }, + "section_title_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, section_title_text links to this" } + ] + }, + "scene2_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "form_action": { + "type": "string", + "description": "Endpoint to submit form data." + }, + "success_title": { + "type": "string", + "description": "(send to device) Title shown before text on successful registration." + }, + "success_text": { + "type": "string", + "description": "Message shown on successful registration." + }, + "error_text": { + "type": "string", + "description": "Message shown if registration failed." + }, + "scene2_email_placeholder_text": { + "type": "string", + "description": "Value to show while input is empty." + }, + "scene2_input_placeholder": { + "type": "string", + "description": "(send to device) Value to show while input is empty." + }, + "scene2_button_label": { + "type": "string", + "description": "Label for form submit button" + }, + "scene2_privacy_html": { + "type": "string", + "description": "Information about how the form data is used." + }, + "scene2_disclaimer_html": { + "type": "string", + "description": "(send to device) Html for disclaimer and link underneath input box." + }, + "scene2_icon": { + "type": "string", + "description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred." + }, + "scene2_icon_dark_theme": { + "type": "string", + "description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred." + }, + "scene2_icon_alt_text": { + "type": "string", + "description": "Alt text describing scene2 icon for screen readers", + "default": "" + }, + "scene2_newsletter": { + "type": "string", + "description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'." + }, + "hidden_inputs": { + "type": "object", + "description": "Each entry represents a hidden input, key is used as value for the name property." + }, + "retry_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for the button in the event of a submission error/failure." + } + ], + "default": "Try again" + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked" + }, + "include_sms": { + "type": "boolean", + "description": "(send to device) Allow users to send an SMS message with the form?" + }, + "message_id_sms": { + "type": "string", + "description": "(send to device) Newsletter/basket id representing the SMS message to be sent." + }, + "message_id_email": { + "type": "string", + "description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/." + }, + "utm_campaign": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_campaign." + }, + "utm_term": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_term." + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + } + } + } + }, + "additionalProperties": false, + "required": ["scene2_text"], + "dependencies": { + "section_title_icon": ["section_title_text"], + "section_title_icon_dark_theme": ["section_title_text"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx new file mode 100644 index 0000000000..b9750e0765 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx @@ -0,0 +1,408 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Button } from "../../components/Button/Button"; +import React from "react"; +import { RichText } from "../../components/RichText/RichText"; +import { safeURI } from "../../template-utils"; +import { SimpleSnippet } from "../SimpleSnippet/SimpleSnippet"; +import { SnippetBase } from "../../components/SnippetBase/SnippetBase"; +import ConditionalWrapper from "../../components/ConditionalWrapper/ConditionalWrapper"; + +// Alt text placeholder in case the prop from the server isn't available +const ICON_ALT_TEXT = ""; + +export class SubmitFormSnippet extends React.PureComponent { + constructor(props) { + super(props); + this.expandSnippet = this.expandSnippet.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this); + this.onInputChange = this.onInputChange.bind(this); + this.state = { + expanded: false, + submitAttempted: false, + signupSubmitted: false, + signupSuccess: false, + disableForm: false, + }; + } + + handleSubmitAttempt() { + if (!this.state.submitAttempted) { + this.setState({ submitAttempted: true }); + } + } + + async handleSubmit(event) { + let json; + + if (this.state.disableForm) { + return; + } + + event.preventDefault(); + this.setState({ disableForm: true }); + this.props.sendUserActionTelemetry({ + event: "CLICK_BUTTON", + event_context: "conversion-subscribe-activation", + id: "NEWTAB_FOOTER_BAR_CONTENT", + }); + + if (this.props.form_method.toUpperCase() === "GET") { + this.props.onBlock({ preventDismiss: true }); + this.refs.form.submit(); + return; + } + + const { url, formData } = this.props.processFormData + ? this.props.processFormData(this.refs.mainInput, this.props) + : { url: this.refs.form.action, formData: new FormData(this.refs.form) }; + + try { + const fetchRequest = new Request(url, { + body: formData, + method: "POST", + credentials: "omit", + }); + const response = await fetch(fetchRequest); // eslint-disable-line fetch-options/no-fetch-credentials + json = await response.json(); + } catch (err) { + console.error(err); + } + + if (json && json.status === "ok") { + this.setState({ signupSuccess: true, signupSubmitted: true }); + if (!this.props.content.do_not_autoblock) { + this.props.onBlock({ preventDismiss: true }); + } + this.props.sendUserActionTelemetry({ + event: "CLICK_BUTTON", + event_context: "subscribe-success", + id: "NEWTAB_FOOTER_BAR_CONTENT", + }); + } else { + console.error( + "There was a problem submitting the form", + json || "[No JSON response]" + ); + this.setState({ signupSuccess: false, signupSubmitted: true }); + this.props.sendUserActionTelemetry({ + event: "CLICK_BUTTON", + event_context: "subscribe-error", + id: "NEWTAB_FOOTER_BAR_CONTENT", + }); + } + + this.setState({ disableForm: false }); + } + + expandSnippet() { + this.props.sendUserActionTelemetry({ + event: "CLICK_BUTTON", + event_context: "scene1-button-learn-more", + id: this.props.UISurface, + }); + + this.setState({ + expanded: true, + signupSuccess: false, + signupSubmitted: false, + }); + } + + renderHiddenFormInputs() { + const { hidden_inputs } = this.props.content; + + if (!hidden_inputs) { + return null; + } + + return Object.keys(hidden_inputs).map((key, idx) => ( + + )); + } + + renderDisclaimer() { + const { content } = this.props; + if (!content.scene2_disclaimer_html) { + return null; + } + return ( +

+ +

+ ); + } + + renderFormPrivacyNotice() { + const { content } = this.props; + if (!content.scene2_privacy_html) { + return null; + } + return ( +

+ + +

+ ); + } + + renderSignupSubmitted() { + const { content } = this.props; + const isSuccess = this.state.signupSuccess; + const successTitle = isSuccess && content.success_title; + const bodyText = isSuccess + ? { success_text: content.success_text } + : { error_text: content.error_text }; + const retryButtonText = content.retry_button_label; + return ( + +
+ {successTitle ? ( +

{successTitle}

+ ) : null} +

+ + {isSuccess ? null : ( + + )} +

+
+
+ ); + } + + onInputChange(event) { + if (!this.props.validateInput) { + return; + } + const hasError = this.props.validateInput( + event.target.value, + this.props.content + ); + event.target.setCustomValidity(hasError); + } + + wrapSectionHeader(url) { + return function (children) { + return {children}; + }; + } + + renderInput() { + const placholder = + this.props.content.scene2_email_placeholder_text || + this.props.content.scene2_input_placeholder; + return ( + + ); + } + + renderForm() { + return ( +
+ {this.renderHiddenFormInputs()} +
+ {this.renderInput()} + +
+ {this.renderFormPrivacyNotice() || this.renderDisclaimer()} +
+ ); + } + + renderScene2Icon() { + const { content } = this.props; + if (!content.scene2_icon) { + return null; + } + + return ( +
+ {content.scene2_icon_alt_text + {content.scene2_icon_alt_text +
+ ); + } + + renderSignupView() { + const { content } = this.props; + const containerClass = `SubmitFormSnippet ${this.props.className}`; + return ( + + {this.renderScene2Icon()} +
+

+ {content.scene2_title && ( +

{content.scene2_title}

+ )}{" "} + {content.scene2_text && ( + + )} +

+
+ {this.renderForm()} +
+ ); + } + + renderSectionHeader() { + const { props } = this; + + // an icon and text must be specified to render the section header + if (props.content.section_title_icon && props.content.section_title_text) { + const sectionTitleIconLight = safeURI(props.content.section_title_icon); + const sectionTitleIconDark = safeURI( + props.content.section_title_icon_dark_theme || + props.content.section_title_icon + ); + const sectionTitleURL = props.content.section_title_url; + + return ( +
+

+ + + + + {props.content.section_title_text} + + +

+
+ ); + } + + return null; + } + + renderSignupViewAlt() { + const { content } = this.props; + const containerClass = `SubmitFormSnippet ${this.props.className} scene2Alt`; + return ( + + {this.renderSectionHeader()} + {this.renderScene2Icon()} +
+

+ {content.scene2_text && ( + + )} +

+ {this.renderForm()} +
+
+ ); + } + + getFirstSceneContent() { + return Object.keys(this.props.content) + .filter(key => key.includes("scene1")) + .reduce((acc, key) => { + acc[key.substr(7)] = this.props.content[key]; + return acc; + }, {}); + } + + render() { + const content = { ...this.props.content, ...this.getFirstSceneContent() }; + + if (this.state.signupSubmitted) { + return this.renderSignupSubmitted(); + } + // Render only scene 2 (signup view). Must check before `renderSignupView` + // to catch the Failure/Try again scenario where we want to return and render + // the scene again. + if (this.props.expandedAlt) { + return this.renderSignupViewAlt(); + } + if (this.state.expanded) { + return this.renderSignupView(); + } + return ( + + ); + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json new file mode 100644 index 0000000000..2a5ebda7e0 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json @@ -0,0 +1,235 @@ +{ + "title": "SubmitFormSnippet", + "description": "A template with two states: a SimpleSnippet and another that contains a form", + "version": "1.2.0", + "type": "object", + "definitions": { + "plainText": { + "description": "Plain text (no HTML allowed)", + "type": "string" + }, + "richText": { + "description": "Text with HTML subset allowed: i, b, u, strong, em, br", + "type": "string" + }, + "link_url": { + "description": "Target for links or buttons", + "type": "string", + "format": "uri" + } + }, + "properties": { + "locale": { + "type": "string", + "description": "Two to five character string for the locale code" + }, + "country": { + "type": "string", + "description": "Two character string for the country code (used for SMS)" + }, + "scene1_title": { + "allof": [ + { "$ref": "#/definitions/plainText" }, + { "description": "snippet title displayed before snippet text" } + ] + }, + "scene1_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_section_title_icon": { + "type": "string", + "description": "Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_icon_dark_theme": { + "type": "string", + "description": "Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display." + }, + "scene1_section_title_text": { + "type": "string", + "description": "Section title text for scene 1. scene1_section_title_icon must also be specified to display." + }, + "scene1_section_title_url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "A url, scene1_section_title_text links to this" } + ] + }, + "scene2_title": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Title displayed before text in scene 2. Should be plain text." + } + ] + }, + "scene2_text": { + "allOf": [ + { "$ref": "#/definitions/richText" }, + { + "description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br" + } + ] + }, + "scene1_icon": { + "type": "string", + "description": "Snippet icon. 64x64px. SVG or PNG preferred." + }, + "scene1_icon_dark_theme": { + "type": "string", + "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred." + }, + "scene1_icon_alt_text": { + "type": "string", + "description": "Alt text describing scene1 icon for screen readers", + "default": "" + }, + "scene1_title_icon": { + "type": "string", + "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene1_title_icon_dark_theme": { + "type": "string", + "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale." + }, + "scene1_title_icon_alt_text": { + "type": "string", + "description": "Alt text describing scene1 title icon for screen readers", + "default": "" + }, + "form_action": { + "type": "string", + "description": "Endpoint to submit form data." + }, + "success_title": { + "type": "string", + "description": "(send to device) Title shown before text on successful registration." + }, + "success_text": { + "type": "string", + "description": "Message shown on successful registration." + }, + "error_text": { + "type": "string", + "description": "Message shown if registration failed." + }, + "scene2_email_placeholder_text": { + "type": "string", + "description": "Value to show while input is empty." + }, + "scene2_input_placeholder": { + "type": "string", + "description": "(send to device) Value to show while input is empty." + }, + "scene2_button_label": { + "type": "string", + "description": "Label for form submit button" + }, + "scene2_privacy_html": { + "type": "string", + "description": "Information about how the form data is used." + }, + "scene2_disclaimer_html": { + "type": "string", + "description": "(send to device) Html for disclaimer and link underneath input box." + }, + "scene2_dismiss_button_text": { + "type": "string", + "description": "Label for the dismiss button when the sign-up form is expanded." + }, + "scene2_icon": { + "type": "string", + "description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred." + }, + "scene2_icon_dark_theme": { + "type": "string", + "description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred." + }, + "scene2_icon_alt_text": { + "type": "string", + "description": "Alt text describing scene2 icon for screen readers", + "default": "" + }, + "scene2_newsletter": { + "type": "string", + "description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'." + }, + "hidden_inputs": { + "type": "object", + "description": "Each entry represents a hidden input, key is used as value for the name property." + }, + "scene1_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for a button next to main snippet text that links to button_url. Requires button_url." + } + ] + }, + "scene1_button_color": { + "type": "string", + "description": "The text color of the button. Valid CSS color." + }, + "scene1_button_background_color": { + "type": "string", + "description": "The background color of the button. Valid CSS color." + }, + "retry_button_label": { + "allOf": [ + { "$ref": "#/definitions/plainText" }, + { + "description": "Text for the button in the event of a submission error/failure." + } + ], + "default": "Try again" + }, + "do_not_autoblock": { + "type": "boolean", + "description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked" + }, + "include_sms": { + "type": "boolean", + "description": "(send to device) Allow users to send an SMS message with the form?" + }, + "message_id_sms": { + "type": "string", + "description": "(send to device) Newsletter/basket id representing the SMS message to be sent." + }, + "message_id_email": { + "type": "string", + "description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/." + }, + "utm_campaign": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_campaign." + }, + "utm_term": { + "type": "string", + "description": "(fxa) Value to pass through to GA as utm_term." + }, + "links": { + "additionalProperties": { + "url": { + "allOf": [ + { "$ref": "#/definitions/link_url" }, + { "description": "The url where the link points to." } + ] + }, + "metric": { + "type": "string", + "description": "Custom event name sent with telemetry event." + } + } + } + }, + "additionalProperties": false, + "required": ["scene1_text", "scene2_text", "scene1_button_label"], + "dependencies": { + "scene1_button_color": ["scene1_button_label"], + "scene1_button_background_color": ["scene1_button_label"] + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/_SubmitFormSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/_SubmitFormSnippet.scss new file mode 100644 index 0000000000..3c1738aef0 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/_SubmitFormSnippet.scss @@ -0,0 +1,176 @@ +.SubmitFormSnippet { + flex-direction: column; + flex: 1 1 100%; + width: 100%; + + .disclaimerText { + margin: 5px 0 0; + font-size: 12px; + color: var(--newtab-text-secondary-color); + } + + p { + margin: 0; + } + + &.send_to_device_snippet { + text-align: center; + + .message { + font-size: 16px; + margin-bottom: 20px; + } + + .scene2Title { + font-size: 24px; + display: block; + } + } + + .ASRouterButton { + &.primary { + flex: 1 1 0; + } + } + + .scene2Icon { + width: 100%; + margin-bottom: 20px; + + img { + width: 98px; + display: inline-block; + } + } + + .scene2Title { + font-size: inherit; + margin: 0 0 10px; + font-weight: bold; + display: inline; + } + + form { + display: flex; + flex-direction: column; + width: 100%; + } + + .message { + font-size: 14px; + align-self: stretch; + flex: 0 0 100%; + margin-bottom: 10px; + } + + .privacyNotice { + font-size: 12px; + color: var(--newtab-text-secondary-color); + margin-top: 10px; + display: flex; + flex: 0 0 100%; + } + + .innerWrapper { + // https://github.com/mozmeao/snippets/blob/2054899350590adcb3c0b0a341c782b0e2f81d0b/activity-stream/newsletter-subscribe.html#L46 + max-width: 736px; + flex-wrap: wrap; + justify-items: center; + padding-top: 40px; + padding-bottom: 40px; + } + + .footer { + width: 100%; + margin: 0 auto; + text-align: right; + background-color: var(--newtab-background-color); + padding: 10px 0; + + .footer-content { + margin: 0 auto; + max-width: 768px; + width: 100%; + text-align: right; + + [dir='rtl'] & { + text-align: left; + } + } + } + + input { + &.mainInput { + border-radius: 2px; + background-color: var(--newtab-background-color-secondary); + border: $input-border; + padding: 0 8px; + height: 100%; + font-size: 14px; + width: 50%; + + &.clean { + &:invalid, + &:required { + box-shadow: none; + } + } + + &:focus { + border: $input-border-active; + box-shadow: var(--newtab-textbox-focus-boxshadow); + } + } + } + + &.scene2Alt { + text-align: start; + + .scene2Icon { + flex: 1; + margin-bottom: 0; + } + + .message { + flex: 5; + margin-bottom: 0; + + p { + margin-bottom: 10px; + } + } + + .section-header { + width: 100%; + + .icon { + width: 16px; + height: 16px; + } + } + + .section-title { + font-size: 13px; + } + + .section-title a { + color: var(--newtab-text-primary-color); + font-weight: inherit; + text-decoration: none; + } + + .innerWrapper { + padding: 0 0 16px; + } + } +} + +.submissionStatus { + text-align: center; + font-size: 14px; + padding: 20px 0; + + .submitStatusTitle { + font-size: 20px; + } +} diff --git a/browser/components/newtab/content-src/asrouter/templates/ToastNotification/ToastNotification.schema.json b/browser/components/newtab/content-src/asrouter/templates/ToastNotification/ToastNotification.schema.json new file mode 100644 index 0000000000..c6d917d235 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/ToastNotification/ToastNotification.schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///ToastNotification.schema.json", + "title": "ToastNotification", + "description": "A template for toast notifications displayed by the Alert service.", + "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "title": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "Id of localized string or message override of toast notification title" + }, + "body": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "Id of localized string or message override of toast notification body" + }, + "icon_url": { + "description": "The URL of the image used as an icon of the toast notification.", + "type": "string", + "format": "moz-url-format" + }, + "image_url": { + "description": "The URL of an image to be displayed as part of the notification.", + "type": "string", + "format": "moz-url-format" + }, + "launch_url": { + "description": "The URL to launch when the notification or an action button is clicked.", + "type": "string", + "format": "moz-url-format" + }, + "requireInteraction": { + "type": "boolean", + "description": "Whether the toast notification should remain active until the user clicks or dismisses it, rather than closing automatically." + }, + "tag": { + "type": "string", + "description": "An identifying tag for the toast notification." + }, + "data": { + "type": "object", + "description": "Arbitrary data associated with the toast notification." + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", + "description": "The action text to be shown to the user." + }, + "action": { + "type": "string", + "description": "Opaque identifer that identifies action." + }, + "iconURL": { + "type": "string", + "format": "uri", + "description": "URL of an icon to display with the action." + }, + "windowsSystemActivationType": { + "type": "boolean", + "description": "Whether to have Windows process the given `action`." + } + }, + "required": ["action", "title"], + "additionalProperties": true + } + } + }, + "additionalProperties": true, + "required": ["title", "body"] + }, + "template": { + "type": "string", + "const": "toast_notification" + } + }, + "required": ["content", "targeting", "template", "trigger"], + "additionalProperties": true +} diff --git a/browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx b/browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx new file mode 100644 index 0000000000..57f8afa6f5 --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { EOYSnippet } from "./EOYSnippet/EOYSnippet"; +import { FXASignupSnippet } from "./FXASignupSnippet/FXASignupSnippet"; +import { NewsletterSnippet } from "./NewsletterSnippet/NewsletterSnippet"; +import { + SendToDeviceSnippet, + SendToDeviceScene2Snippet, +} from "./SendToDeviceSnippet/SendToDeviceSnippet"; +import { SimpleBelowSearchSnippet } from "./SimpleBelowSearchSnippet/SimpleBelowSearchSnippet"; +import { SimpleSnippet } from "./SimpleSnippet/SimpleSnippet"; + +// Key names matching schema name of templates +export const SnippetsTemplates = { + simple_snippet: SimpleSnippet, + newsletter_snippet: NewsletterSnippet, + fxa_signup_snippet: FXASignupSnippet, + send_to_device_snippet: SendToDeviceSnippet, + send_to_device_scene2_snippet: SendToDeviceScene2Snippet, + eoy_snippet: EOYSnippet, + simple_below_search_snippet: SimpleBelowSearchSnippet, +}; diff --git a/browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx b/browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx new file mode 100644 index 0000000000..3aab52cdff --- /dev/null +++ b/browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export function A11yLinkButton(props) { + // function for merging classes, if necessary + let className = "a11y-link-button"; + if (props.className) { + className += ` ${props.className}`; + } + return ( + + ); +} diff --git a/browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss b/browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss new file mode 100644 index 0000000000..c87fc93b60 --- /dev/null +++ b/browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss @@ -0,0 +1,13 @@ + +.a11y-link-button { + border: 0; + padding: 0; + cursor: pointer; + text-align: unset; + color: var(--newtab-primary-action-background); + + &:hover, + &:focus { + text-decoration: underline; + } +} diff --git a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx new file mode 100644 index 0000000000..3762be9c99 --- /dev/null +++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx @@ -0,0 +1,1967 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { ASRouterUtils } from "../../asrouter/asrouter-utils"; +import { connect } from "react-redux"; +import React from "react"; +import { SimpleHashRouter } from "./SimpleHashRouter"; +import { CopyButton } from "./CopyButton"; + +const Row = props => ( + + {props.children} + +); + +function relativeTime(timestamp) { + if (!timestamp) { + return ""; + } + const seconds = Math.floor((Date.now() - timestamp) / 1000); + const minutes = Math.floor((Date.now() - timestamp) / 60000); + if (seconds < 2) { + return "just now"; + } else if (seconds < 60) { + return `${seconds} seconds ago`; + } else if (minutes === 1) { + return "1 minute ago"; + } else if (minutes < 600) { + return `${minutes} minutes ago`; + } + return new Date(timestamp).toLocaleString(); +} + +const LAYOUT_VARIANTS = { + basic: "Basic default layout (on by default in nightly)", + staging_spocs: "A layout with all spocs shown", + "dev-test-all": + "A little bit of everything. Good layout for testing all components", + "dev-test-feeds": "Stress testing for slow feeds", +}; + +export class ToggleStoryButton extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.props.onClick(this.props.story); + } + + render() { + return ; + } +} + +export class ToggleMessageJSON extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.props.toggleJSON(this.props.msgId); + } + + render() { + let iconName = this.props.isCollapsed + ? "icon icon-arrowhead-forward-small" + : "icon icon-arrowhead-down-small"; + return ( + + ); + } +} + +export class TogglePrefCheckbox extends React.PureComponent { + constructor(props) { + super(props); + this.onChange = this.onChange.bind(this); + } + + onChange(event) { + this.props.onChange(this.props.pref, event.target.checked); + } + + render() { + return ( + <> + {" "} + {this.props.pref}{" "} + + ); + } +} + +export class Personalization extends React.PureComponent { + constructor(props) { + super(props); + this.togglePersonalization = this.togglePersonalization.bind(this); + } + + togglePersonalization() { + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE, + }) + ); + } + + render() { + const { lastUpdated, initialized } = this.props.state.Personalization; + return ( + + + + + + + + + + + + + + + +
+ + Personalization Last Updated{relativeTime(lastUpdated) || "(no data)"}Personalization Initialized{initialized ? "true" : "false"}
+
+ ); + } +} + +export class DiscoveryStreamAdmin extends React.PureComponent { + constructor(props) { + super(props); + this.restorePrefDefaults = this.restorePrefDefaults.bind(this); + this.setConfigValue = this.setConfigValue.bind(this); + this.expireCache = this.expireCache.bind(this); + this.refreshCache = this.refreshCache.bind(this); + this.idleDaily = this.idleDaily.bind(this); + this.systemTick = this.systemTick.bind(this); + this.syncRemoteSettings = this.syncRemoteSettings.bind(this); + this.changeEndpointVariant = this.changeEndpointVariant.bind(this); + this.onStoryToggle = this.onStoryToggle.bind(this); + this.state = { + toggledStories: {}, + }; + } + + setConfigValue(name, value) { + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE, + data: { name, value }, + }) + ); + } + + restorePrefDefaults(event) { + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS, + }) + ); + } + + refreshCache() { + const { config } = this.props.state.DiscoveryStream; + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_CONFIG_CHANGE, + data: config, + }) + ); + } + + dispatchSimpleAction(type) { + this.props.dispatch( + ac.OnlyToMain({ + type, + }) + ); + } + + systemTick() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYSTEM_TICK); + } + + expireCache() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE); + } + + idleDaily() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_IDLE_DAILY); + } + + syncRemoteSettings() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS); + } + + changeEndpointVariant(event) { + const endpoint = this.props.state.DiscoveryStream.config.layout_endpoint; + if (endpoint) { + this.setConfigValue( + "layout_endpoint", + endpoint.replace( + /layout_variant=.+/, + `layout_variant=${event.target.value}` + ) + ); + } + } + + renderComponent(width, component) { + return ( + + + + + + + + + + + {component.feed && this.renderFeed(component.feed)} + +
Type{component.type}Width{width}
+ ); + } + + isCurrentVariant(id) { + const endpoint = this.props.state.DiscoveryStream.config.layout_endpoint; + const isMatch = endpoint && !!endpoint.match(`layout_variant=${id}`); + return isMatch; + } + + renderFeedData(url) { + const { feeds } = this.props.state.DiscoveryStream; + const feed = feeds.data[url].data; + return ( + +

Feed url: {url}

+ + + {feed.recommendations?.map(story => this.renderStoryData(story))} + +
+
+ ); + } + + renderFeedsData() { + const { feeds } = this.props.state.DiscoveryStream; + return ( + + {Object.keys(feeds.data).map(url => this.renderFeedData(url))} + + ); + } + + renderSpocs() { + const { spocs } = this.props.state.DiscoveryStream; + let spocsData = []; + if (spocs.data && spocs.data.spocs && spocs.data.spocs.items) { + spocsData = spocs.data.spocs.items || []; + } + + return ( + + + + + + + + + + + + +
spocs_endpoint{spocs.spocs_endpoint}Data last fetched{relativeTime(spocs.lastUpdated)}
+

Spoc data

+ + {spocsData.map(spoc => this.renderStoryData(spoc))} +
+

Spoc frequency caps

+ + + {spocs.frequency_caps.map(spoc => this.renderStoryData(spoc))} + +
+
+ ); + } + + onStoryToggle(story) { + const { toggledStories } = this.state; + this.setState({ + toggledStories: { + ...toggledStories, + [story.id]: !toggledStories[story.id], + }, + }); + } + + renderStoryData(story) { + let storyData = ""; + if (this.state.toggledStories[story.id]) { + storyData = JSON.stringify(story, null, 2); + } + return ( + + + + {story.id}
+
+ + + +
{storyData}
+ + + ); + } + + renderFeed(feed) { + const { feeds } = this.props.state.DiscoveryStream; + if (!feed.url) { + return null; + } + return ( + + + Feed url + {feed.url} + + + Data last fetched + + {relativeTime( + feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null + ) || "(no data)"} + + + + ); + } + + render() { + const prefToggles = "enabled hardcoded_layout show_spocs collapsible".split( + " " + ); + const { config, lastUpdated, layout } = this.props.state.DiscoveryStream; + const personalized = + this.props.otherPrefs["discoverystream.personalization.enabled"]; + return ( +
+ {" "} + +
+ {" "} + {" "} + +
+ + + + {prefToggles.map(pref => ( + + + + ))} + +
+ +
+

Endpoint variant

+

+ You can also change this manually by changing this pref:{" "} + browser.newtabpage.activity-stream.discoverystream.config +

+ + + {Object.keys(LAYOUT_VARIANTS).map(id => ( + + + + + + ))} + +
+ + {id}{LAYOUT_VARIANTS[id]}
+

Caching info

+ + + + + + + +
Data last fetched{relativeTime(lastUpdated) || "(no data)"}
+

Layout

+ {layout.map((row, rowIndex) => ( +
+ {row.components.map((component, componentIndex) => ( +
+ {this.renderComponent(row.width, component)} +
+ ))} +
+ ))} +

Personalization

+ +

Spocs

+ {this.renderSpocs()} +

Feeds Data

+ {this.renderFeedsData()} +
+ ); + } +} + +export class ASRouterAdminInner extends React.PureComponent { + constructor(props) { + super(props); + this.handleEnabledToggle = this.handleEnabledToggle.bind(this); + this.handleUserPrefToggle = this.handleUserPrefToggle.bind(this); + this.onChangeMessageFilter = this.onChangeMessageFilter.bind(this); + this.onChangeMessageGroupsFilter = + this.onChangeMessageGroupsFilter.bind(this); + this.unblockAll = this.unblockAll.bind(this); + this.handleClearAllImpressionsByProvider = + this.handleClearAllImpressionsByProvider.bind(this); + this.handleExpressionEval = this.handleExpressionEval.bind(this); + this.onChangeTargetingParameters = + this.onChangeTargetingParameters.bind(this); + this.onChangeAttributionParameters = + this.onChangeAttributionParameters.bind(this); + this.setAttribution = this.setAttribution.bind(this); + this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this); + this.onNewTargetingParams = this.onNewTargetingParams.bind(this); + this.handleOpenPB = this.handleOpenPB.bind(this); + this.selectPBMessage = this.selectPBMessage.bind(this); + this.resetPBJSON = this.resetPBJSON.bind(this); + this.resetPBMessageState = this.resetPBMessageState.bind(this); + this.toggleJSON = this.toggleJSON.bind(this); + this.toggleAllMessages = this.toggleAllMessages.bind(this); + this.resetGroups = this.resetGroups.bind(this); + this.onMessageFromParent = this.onMessageFromParent.bind(this); + this.setStateFromParent = this.setStateFromParent.bind(this); + this.setState = this.setState.bind(this); + this.state = { + messageFilter: "all", + messageGroupsFilter: "all", + collapsedMessages: [], + modifiedMessages: [], + selectedPBMessage: "", + evaluationStatus: {}, + stringTargetingParameters: null, + newStringTargetingParameters: null, + copiedToClipboard: false, + attributionParameters: { + source: "addons.mozilla.org", + medium: "referral", + campaign: "non-fx-button", + content: `rta:${btoa("uBlock0@raymondhill.net")}`, + experiment: "ua-onboarding", + variation: "chrome", + ua: "Google Chrome 123", + dltoken: "00000000-0000-0000-0000-000000000000", + }, + }; + } + + onMessageFromParent({ type, data }) { + // These only exists due to onPrefChange events in ASRouter + switch (type) { + case "UpdateAdminState": { + this.setStateFromParent(data); + break; + } + } + } + + setStateFromParent(data) { + this.setState(data); + if (!this.state.stringTargetingParameters) { + const stringTargetingParameters = {}; + for (const param of Object.keys(data.targetingParameters)) { + stringTargetingParameters[param] = JSON.stringify( + data.targetingParameters[param], + null, + 2 + ); + } + this.setState({ stringTargetingParameters }); + } + } + + componentWillMount() { + ASRouterUtils.addListener(this.onMessageFromParent); + const endpoint = ASRouterUtils.getPreviewEndpoint(); + ASRouterUtils.sendMessage({ + type: "ADMIN_CONNECT_STATE", + data: { endpoint }, + }).then(this.setStateFromParent); + } + + handleBlock(msg) { + return () => ASRouterUtils.blockById(msg.id); + } + + handleUnblock(msg) { + return () => ASRouterUtils.unblockById(msg.id); + } + + resetJSON(msg) { + // reset the displayed JSON for the given message + document.getElementById(`${msg.id}-textarea`).value = JSON.stringify( + msg, + null, + 2 + ); + // remove the message from the list of modified IDs + let index = this.state.modifiedMessages.indexOf(msg.id); + this.setState(prevState => ({ + modifiedMessages: [ + ...prevState.modifiedMessages.slice(0, index), + ...prevState.modifiedMessages.slice(index + 1), + ], + })); + } + + handleOverride(id) { + return () => + ASRouterUtils.overrideMessage(id).then(state => { + this.setStateFromParent(state); + this.props.notifyContent({ + message: state.message, + }); + }); + } + + resetPBMessageState() { + // Iterate over Private Browsing messages and block/unblock each one to clear impressions + const PBMessages = this.state.messages.filter( + message => message.template === "pb_newtab" + ); // messages from state go here + + PBMessages.forEach(message => { + if (message?.id) { + ASRouterUtils.blockById(message.id); + ASRouterUtils.unblockById(message.id); + } + }); + // Clear the selected messages & radio buttons + document.getElementById("clear radio").checked = true; + this.selectPBMessage("clear"); + } + + resetPBJSON(msg) { + // reset the displayed JSON for the given message + document.getElementById(`${msg.id}-textarea`).value = JSON.stringify( + msg, + null, + 2 + ); + } + + handleOpenPB() { + ASRouterUtils.sendMessage({ + type: "FORCE_PRIVATE_BROWSING_WINDOW", + data: { message: { content: this.state.selectedPBMessage } }, + }); + } + + expireCache() { + ASRouterUtils.sendMessage({ type: "EXPIRE_QUERY_CACHE" }); + } + + resetPref() { + ASRouterUtils.sendMessage({ type: "RESET_PROVIDER_PREF" }); + } + + resetGroups(id, value) { + ASRouterUtils.sendMessage({ + type: "RESET_GROUPS_STATE", + }).then(this.setStateFromParent); + } + + handleExpressionEval() { + const context = {}; + for (const param of Object.keys(this.state.stringTargetingParameters)) { + const value = this.state.stringTargetingParameters[param]; + context[param] = value ? JSON.parse(value) : null; + } + ASRouterUtils.sendMessage({ + type: "EVALUATE_JEXL_EXPRESSION", + data: { + expression: this.refs.expressionInput.value, + context, + }, + }).then(this.setStateFromParent); + } + + onChangeTargetingParameters(event) { + const { name } = event.target; + const { value } = event.target; + + this.setState(({ stringTargetingParameters }) => { + let targetingParametersError = null; + const updatedParameters = { ...stringTargetingParameters }; + updatedParameters[name] = value; + try { + JSON.parse(value); + } catch (e) { + console.error(`Error parsing value of parameter ${name}`); + targetingParametersError = { id: name }; + } + + return { + copiedToClipboard: false, + evaluationStatus: {}, + stringTargetingParameters: updatedParameters, + targetingParametersError, + }; + }); + } + + unblockAll() { + return ASRouterUtils.sendMessage({ + type: "UNBLOCK_ALL", + }).then(this.setStateFromParent); + } + + handleClearAllImpressionsByProvider() { + const providerId = this.state.messageFilter; + if (!providerId) { + return; + } + const userPrefInfo = this.state.userPrefs; + + const isUserEnabled = + providerId in userPrefInfo ? userPrefInfo[providerId] : true; + + ASRouterUtils.sendMessage({ + type: "DISABLE_PROVIDER", + data: providerId, + }); + if (!isUserEnabled) { + ASRouterUtils.sendMessage({ + type: "SET_PROVIDER_USER_PREF", + data: { id: providerId, value: true }, + }); + } + ASRouterUtils.sendMessage({ + type: "ENABLE_PROVIDER", + data: providerId, + }); + } + + handleEnabledToggle(event) { + const provider = this.state.providerPrefs.find( + p => p.id === event.target.dataset.provider + ); + const userPrefInfo = this.state.userPrefs; + + const isUserEnabled = + provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; + const isSystemEnabled = provider.enabled; + const isEnabling = event.target.checked; + + if (isEnabling) { + if (!isUserEnabled) { + ASRouterUtils.sendMessage({ + type: "SET_PROVIDER_USER_PREF", + data: { id: provider.id, value: true }, + }); + } + if (!isSystemEnabled) { + ASRouterUtils.sendMessage({ + type: "ENABLE_PROVIDER", + data: provider.id, + }); + } + } else { + ASRouterUtils.sendMessage({ + type: "DISABLE_PROVIDER", + data: provider.id, + }); + } + + this.setState({ messageFilter: "all" }); + } + + handleUserPrefToggle(event) { + const action = { + type: "SET_PROVIDER_USER_PREF", + data: { id: event.target.dataset.provider, value: event.target.checked }, + }; + ASRouterUtils.sendMessage(action); + this.setState({ messageFilter: "all" }); + } + + onChangeMessageFilter(event) { + this.setState({ messageFilter: event.target.value }); + } + + onChangeMessageGroupsFilter(event) { + this.setState({ messageGroupsFilter: event.target.value }); + } + + // Simulate a copy event that sets to clipboard all targeting paramters and values + onCopyTargetingParams(event) { + const stringTargetingParameters = { + ...this.state.stringTargetingParameters, + }; + for (const key of Object.keys(stringTargetingParameters)) { + // If the value is not set the parameter will be lost when we stringify + if (stringTargetingParameters[key] === undefined) { + stringTargetingParameters[key] = null; + } + } + const setClipboardData = e => { + e.preventDefault(); + e.clipboardData.setData( + "text", + JSON.stringify(stringTargetingParameters, null, 2) + ); + document.removeEventListener("copy", setClipboardData); + this.setState({ copiedToClipboard: true }); + }; + + document.addEventListener("copy", setClipboardData); + + document.execCommand("copy"); + } + + onNewTargetingParams(event) { + this.setState({ newStringTargetingParameters: event.target.value }); + event.target.classList.remove("errorState"); + this.refs.targetingParamsEval.innerText = ""; + + try { + const stringTargetingParameters = JSON.parse(event.target.value); + this.setState({ stringTargetingParameters }); + } catch (e) { + event.target.classList.add("errorState"); + this.refs.targetingParamsEval.innerText = e.message; + } + } + + toggleJSON(msgId) { + if (this.state.collapsedMessages.includes(msgId)) { + let index = this.state.collapsedMessages.indexOf(msgId); + this.setState(prevState => ({ + collapsedMessages: [ + ...prevState.collapsedMessages.slice(0, index), + ...prevState.collapsedMessages.slice(index + 1), + ], + })); + } else { + this.setState(prevState => ({ + collapsedMessages: prevState.collapsedMessages.concat(msgId), + })); + } + } + + handleChange(msgId) { + if (!this.state.modifiedMessages.includes(msgId)) { + this.setState(prevState => ({ + modifiedMessages: prevState.modifiedMessages.concat(msgId), + })); + } + } + + renderMessageItem(msg) { + const isBlockedByGroup = this.state.groups + .filter(group => msg.groups.includes(group.id)) + .some(group => !group.enabled); + const msgProvider = + this.state.providers.find(provider => provider.id === msg.provider) || {}; + const isProviderExcluded = + msgProvider.exclude && msgProvider.exclude.includes(msg.id); + const isMessageBlocked = + this.state.messageBlockList.includes(msg.id) || + this.state.messageBlockList.includes(msg.campaign); + const isBlocked = + isMessageBlocked || isBlockedByGroup || isProviderExcluded; + const impressions = this.state.messageImpressions[msg.id] + ? this.state.messageImpressions[msg.id].length + : 0; + const isCollapsed = this.state.collapsedMessages.includes(msg.id); + const isModified = this.state.modifiedMessages.includes(msg.id); + const aboutMessagePreviewSupported = [ + "infobar", + "spotlight", + "cfr_doorhanger", + ].includes(msg.template); + + let itemClassName = "message-item"; + if (isBlocked) { + itemClassName += " blocked"; + } + + return ( + + + + {msg.id}
+
+ + + + + + + { + // eslint-disable-next-line no-nested-ternary + isBlocked ? null : isModified ? ( + + ) : ( + + ) + } + {isBlocked ? null : ( + + )} + {aboutMessagePreviewSupported ? ( + + `about:messagepreview?json=${encodeURIComponent(btoa(text))}` + } + label="Share" + copiedLabel="Copied!" + inputSelector={`#${msg.id}-textarea`} + className={"button share"} + /> + ) : null} +
({impressions} impressions) + + + {isBlocked && ( + + Block reason: + {isBlockedByGroup && " Blocked by group"} + {isProviderExcluded && " Excluded by provider"} + {isMessageBlocked && " Message blocked"} + + )} + +
+              
+            
+ + + + ); + } + + selectPBMessage(msgId) { + if (msgId === "clear") { + this.setState({ + selectedPBMessage: "", + }); + } else { + let selected = document.getElementById(`${msgId} radio`); + let msg = JSON.parse(document.getElementById(`${msgId}-textarea`).value); + + if (selected.checked) { + this.setState({ + selectedPBMessage: msg?.content, + }); + } else { + this.setState({ + selectedPBMessage: "", + }); + } + } + } + + modifyJson(content) { + const message = JSON.parse( + document.getElementById(`${content.id}-textarea`).value + ); + return ASRouterUtils.modifyMessageJson(message).then(state => { + this.setStateFromParent(state); + this.props.notifyContent({ + message: state.message, + }); + }); + } + + renderPBMessageItem(msg) { + const isBlocked = + this.state.messageBlockList.includes(msg.id) || + this.state.messageBlockList.includes(msg.campaign); + const impressions = this.state.messageImpressions[msg.id] + ? this.state.messageImpressions[msg.id].length + : 0; + + const isCollapsed = this.state.collapsedMessages.includes(msg.id); + + let itemClassName = "message-item"; + if (isBlocked) { + itemClassName += " blocked"; + } + + return ( + + + + {msg.id}
+
({impressions} impressions) +
+ + + + + + this.selectPBMessage(msg.id)} + disabled={isBlocked} + /> + + + + +
+            
+          
+ + + ); + } + + toggleAllMessages(messagesToShow) { + if (this.state.collapsedMessages.length) { + this.setState({ + collapsedMessages: [], + }); + } else { + Array.prototype.forEach.call(messagesToShow, msg => { + this.setState(prevState => ({ + collapsedMessages: prevState.collapsedMessages.concat(msg.id), + })); + }); + } + } + + renderMessages() { + if (!this.state.messages) { + return null; + } + const messagesToShow = + this.state.messageFilter === "all" + ? this.state.messages + : this.state.messages.filter( + message => + message.provider === this.state.messageFilter && + message.template !== "pb_newtab" + ); + + return ( +
+ +

+ {" "} + + To modify a message, change the JSON and click 'Modify' to see your + changes. Click 'Reset' to restore the JSON to the original. Click + 'Share' to copy a link to the clipboard that can be used to preview + the message by opening the link in Nightly/local builds. + +

+ + + {messagesToShow.map(msg => this.renderMessageItem(msg))} + +
+
+ ); + } + + renderMessagesByGroup() { + if (!this.state.messages) { + return null; + } + const messagesToShow = + this.state.messageGroupsFilter === "all" + ? this.state.messages.filter(m => m.groups.length) + : this.state.messages.filter(message => + message.groups.includes(this.state.messageGroupsFilter) + ); + + return ( + + {messagesToShow.map(msg => this.renderMessageItem(msg))} +
+ ); + } + + renderPBMessages() { + if (!this.state.messages) { + return null; + } + const messagesToShow = this.state.messages.filter( + message => message.template === "pb_newtab" + ); + return ( + + + {messagesToShow.map(msg => this.renderPBMessageItem(msg))} + +
+ ); + } + + renderMessageFilter() { + if (!this.state.providers) { + return null; + } + + return ( +

+ + Show messages from{" "} + + {this.state.messageFilter !== "all" && + !this.state.messageFilter.includes("_local_testing") ? ( + + ) : null} +

+ ); + } + + renderMessageGroupsFilter() { + if (!this.state.groups) { + return null; + } + + return ( +

+ Show messages from {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +

+ ); + } + + renderTableHead() { + return ( + + + + Provider ID + Source + Cohort + Last Updated + + + ); + } + + renderProviders() { + const providersConfig = this.state.providerPrefs; + const providerInfo = this.state.providers; + const userPrefInfo = this.state.userPrefs; + + return ( + + {this.renderTableHead()} + + {providersConfig.map((provider, i) => { + const isTestProvider = provider.id.includes("_local_testing"); + const info = providerInfo.find(p => p.id === provider.id) || {}; + const isUserEnabled = + provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; + const isSystemEnabled = isTestProvider || provider.enabled; + + let label = "local"; + if (provider.type === "remote") { + label = ( + + endpoint ( + + {info.url} + + ) + + ); + } else if (provider.type === "remote-settings") { + label = `remote settings (${provider.collection})`; + } else if (provider.type === "remote-experiments") { + label = ( + + remote settings ( + + nimbus-desktop-experiments + + ) + + ); + } + + let reasonsDisabled = []; + if (!isSystemEnabled) { + reasonsDisabled.push("system pref"); + } + if (!isUserEnabled) { + reasonsDisabled.push("user pref"); + } + if (reasonsDisabled.length) { + label = `disabled via ${reasonsDisabled.join(", ")}`; + } + + return ( + + + + + + + + ); + })} + +
+ {isTestProvider ? ( + + ) : ( + + )} + {provider.id} + + {label} + + {provider.cohort} + {info.lastUpdated + ? new Date(info.lastUpdated).toLocaleString() + : ""} +
+ ); + } + + renderTargetingParameters() { + // There was no error and the result is truthy + const success = + this.state.evaluationStatus.success && + !!this.state.evaluationStatus.result; + const result = + JSON.stringify(this.state.evaluationStatus.result, null, 2) || + "(Empty result)"; + + return ( + + + + + + +
+

Evaluate JEXL expression

+
+

+