diff --git a/package-lock.json b/package-lock.json index 20014665d..78589f5f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fx-private-relay-add-on", - "version": "2.6.1", + "version": "2.7.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index a168d2b7e..1a72f6515 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fx-private-relay-add-on", - "version": "2.6.1", + "version": "2.7.0", "description": "Firefox Relay", "main": "index.js", "devDependencies": { diff --git a/src/css/global.css b/src/css/global.css index 277a37c54..148a60e86 100644 --- a/src/css/global.css +++ b/src/css/global.css @@ -15,6 +15,7 @@ --layoutLg: 64px; --layoutXl: 96px; --layout2xl: 192px; + --layout3xl: 256px; /* Border Radius */ --borderRadiusXs: 2px; @@ -23,6 +24,7 @@ --borderRadiusLg: 16px; /* Width Tokens */ + --content2xs: 256px; --contentXs: 304px; --contentSm: 432px; --contentMd: 688px; @@ -35,6 +37,21 @@ --screenLg: 1024px; --screenXl: 1312px; + /* Typography Tokens */ + --fontSizeTitle2xl: 64px; + --fontSizeTitleXl: 56px; + --fontSizeTitleLg: 48px; + --fontSizeTitleMd: 40px; + --fontSizeTitleSm: 32px; + --fontSizeTitleXs: 24px; + --fontSizeTitle2xs: 20px; + --fontSizeTitle3xs: 16px; + --fontSizeBodyXl: 21px; + --fontSizeBodyLg: 18px; + --fontSizeBodyMd: 16px; + --fontSizeBodySm: 14px; + --fontSizeBodyXs: 12px; + /* Box Shadow Tokens */ --boxShadowSm: 0 8px 12px 1px rgba(29, 17, 51, .04), 0 3px 16px 2px rgba(9, 32, 77, .12), 0 5px 10px -3px rgba(29, 17, 51, .12); --boxShadowMd: 0 16px 24px 2px rgba(29, 17, 51, .04), 0 6px 32px 4px rgba(9, 32, 77, .12), 0 8px 12px -5px rgba(29, 17, 51, .12); @@ -43,16 +60,22 @@ /* Primary Nebula Colors */ --colorInformational: #0060df; --colorInformationalActive: #054096; - --colorInformationalHover: #bcd0ec; + --colorInformationalHover: #0250bb; --colorInformationalFocus: rgba(0, 96, 223, 0.4); /* This is the RGB value of #0060df @ 40% opacity */ --colorInformationalDisabled: #C9D9F3; /* Nebula Colors: Button: Error */ - --colorError: #FF4F5E; - --colorErrorActive: #C50042; - --colorErrorHover: #E22850; + --colorError: #E22850; + --colorErrorActive: #810220; + --colorErrorHover: #C50042; --colorErrorFocus: #FFBDC5 ; + /* Nebula Colors: Button: Warning */ + --colorWarning: #ffa436; + --colorWarningActive: #c45a27; + --colorWarningHover: #e27f2e; + --colorWarningFocus: #ffd5b2; + /* Nebula Colors: Blues */ --colorBlue90: #09204d; --colorBlue80: #073072; @@ -65,6 +88,18 @@ --colorBlue10: #80ebff; --colorBlue05: #aaf2ff; + /* Nebula Colors: Green */ + --colorGreen90: #00736c; + --colorGreen80: #00a49a; + --colorGreen70: #1cc4a0; + --colorGreen60: #3ad4b3; + --colorGreen50: #3fe1b0; + --colorGreen40: #54ffbd; + --colorGreen30: #88ffd1; + --colorGreen20: #b3ffe3; + --colorGreen10: #d1ffee; + --colorGreen05: #e3fff3; + /* Nebula Colors: Grays */ --colorBlack: #000000; --colorGrey60: #0c0c0d; @@ -87,6 +122,18 @@ --colorViolet10: #f6b8ff; --colorViolet05: #f7e2ff; + /* Nebula Colors: Purple */ + --colorPurple90: #321c64; + --colorPurple80: #45278d; + --colorPurple70: #592acb; + --colorPurple60: #7542e5; + --colorPurple50: #9059ff; + --colorPurple40: #ab71ff; + --colorPurple30: #c689ff; + --colorPurple20: #cb9eff; + --colorPurple10: #d9bfff; + --colorPurple05: #e7dfff; + /* Nebula Colors: Whites */ --colorWhite: #ffffff; @@ -155,6 +202,7 @@ color: var(--colorInformational); font-family: var(--fontStackFirefox); font-weight: 400; + background-color: transparent; } .fx-relay-c-button.t-secondary:hover { @@ -258,6 +306,7 @@ background-color: var(--colorWhite); border: 2px solid transparent; outline: 1px solid var(--colorGrey30); + position: relative; } .fx-relay-c-search-input:focus { @@ -265,6 +314,7 @@ border: 2px solid var(--colorInformational); } +.fx-relay-c-search-input.is-active + .fx-relay-c-search-controls, .fx-relay-c-search-input:focus + .fx-relay-c-search-controls { opacity: 1; } diff --git a/src/css/popup.css b/src/css/popup.css index 994c0b571..2b6c25be3 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -1,545 +1,540 @@ +/* Global Panel Styles */ -.fx-relay-panel { +.fx-relay-panel-wrapper { --panelWidth: 360px; /* Any changes to --panelHeight should be reflected in the screen media query */ --panelHeight: 500px; - background-color: var(--colorWhite); + background-color: var(--colorGrey05); min-width: var(--panelWidth); min-height: var(--panelHeight); + max-width: var(--panelWidth); color: var(--relayInk70); overflow: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; box-sizing: border-box; font-family: var(--fontStackBase); + font-size: var(--fontSizeBodyMd); + position: relative; + padding: 0; + margin: 0; } -.fx-relay-panel *, .fx-relay-panel *:before, .fx-relay-panel *:after { +.fx-relay-panel-wrapper *, .fx-relay-panel-wrapper *:before, .fx-relay-panel-wrapper *:after { box-sizing: inherit; } -sign-up-panel { - height: 100%; - display: flex; - flex-direction: column; - font-size: 14px; - position: relative; +/* Utilities */ +.fx-relay-panel-wrapper .is-hidden { + display: none; } -sign-up-panel::after { - height: 3px; - background: var(--relayFxGradient); - content: ""; - position: absolute; - lefT: 0; - right: 0; - top: -1px; - background-size: 120%; +/* Main Content */ + +.fx-relay-panel-content { + padding: var(--spacingMd); } -::-moz-focus-inner { - border: 0; +/* FIXME: Refactor to account for height dynamically */ +/* Magic Number: Custom max-height for masks panel */ +#masks-panel[data-account-level="free"] .fx-relay-panel-content { + max-height: 384px; + overflow: auto; } -p { - margin-top: 0; - line-height: 1.5; - text-align: center; +#masks-panel[data-account-level="premium"] .fx-relay-panel-content { + max-height: 336px; + overflow: auto; } -settings, report-issue, report-issue-success { - background-color: rgba(255, 255, 255, 1); - position: absolute; +/* Header */ + +.fx-relay-menu-header { + border: 0; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.12); + /* Bottom corners only */ + border-radius: 0 0 var(--borderRadiusSm) var(--borderRadiusSm); + background-color: var(--colorWhite); + position: sticky; top: 0; - bottom: 0; - left: 0; - right: 0; - visibility: hidden; - z-index: 2; - transform: translateX(360px); - transition: all 0.5s ease; - font-size: 14px; - line-height: 1.4; - padding: 0 24px; } -.report-issue-content { +.fx-relay-menu-header-logo-bar { display: flex; - flex-direction: column; - gap: 16px; - padding: 0; - border: 0 none; + justify-content: space-between; + width: 100%; + padding: var(--spacingSm); } -.report-section { - margin-bottom: 8px; +.fx-relay-menu-logo { + margin: 0; + padding: 0; display: flex; - gap: 8px; - flex-direction: column; + flex-direction: row; + justify-content: center; + gap: var(--spacingSm); } -.report-label::after { - content: ":"; +.fx-relay-menu-logo-image-fx-relay { + width: 26px; } -.report-section ul { - list-style-type: none; - padding: 0; - margin: 0; +.fx-relay-menu-logo-text { + width: 104px; + /* Optical offset to center "Firefox Relay" text in logo */ + margin-top: 2px; } -.report-section li { +.fx-relay-menu-header-navigation { display: flex; - align-items:flex-start; - margin-bottom: 8px; -} - -.report-section input[type=checkbox]{ - margin-right: 8px; -} - -.report-section input[type=text]{ - width: 100%; - padding: 4px; + gap: var(--spacingXs); } -.report-section input[type=submit]:disabled{ - background: var(--colorInformationalDisabled); +.fx-relay-menu-dashboard-link { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: var(--fontSizeBodySm); + position: relative; + width: 32px; + height: 32px; + appearance: none; + outline: 0; + border: 0; + background-color: transparent; } -.report-success { - padding-top: 24px; +.fx-relay-menu-dashboard-link .news-count { + position: absolute; + right: calc(var(--spacingXs) * -1); + top: calc(var(--spacingXs) * -1); + border-radius: 100%; + width: 16px; + height: 16px; + font-size: var(--fontSizeBodyXs); display: flex; - flex-direction: column; justify-content: center; align-items: center; + background-color: var(--colorError); + color: var(--colorWhite); } -.report-success h1 { - font-weight: 700; - font-size: 16px; +.news-count.is-hidden { + display: none; } -.report-continue { - cursor: pointer; - color: var(--relayBlue3); +/* Default icon width */ +.fx-relay-menu-dashboard-link img { + pointer-events: none; + width: 14px; + filter: grayscale(1); } -.setting { - padding: 12px 0; - border-bottom: 1px solid var(--relayGrey20); - display: flex; - justify-content: space-between; +.fx-relay-menu-dashboard-link:focus-within img, +.fx-relay-menu-dashboard-link.is-active img, +.fx-relay-menu-dashboard-link:hover img { + filter: grayscale(0); } -.flex-col { - flex-direction: column; +.fx-relay-menu-dashboard-link[data-panel-id="settings"] img { + width: 16px; } -.setting-label { - display: inline-block; - color: var(--relayInk70); +.fx-relay-menu-dashboard-link.is-active, +.fx-relay-menu-dashboard-link:hover, +.fx-relay-menu-dashboard-link:focus { + background-color: var(--colorGrey10); + border-radius: 100%; } -a.setting-label { - text-decoration: none; +.fx-relay-menu-dashboard-link.is-active .fx-relay-menu-dashboard-link-tooltip, +.fx-relay-menu-dashboard-link .fx-relay-menu-dashboard-link-tooltip { + display: none; + color: var(--colorWhite); + background-color: var(--colorGrey40); + border-radius: var(--borderRadiusSm); + padding: var(--spacingXs) var(--spacingSm); + position: absolute; + top: calc(100% + var(--spacingXs)); + right: 0; + white-space: nowrap; } -a.setting-label:hover { - background-color: var(--relayGrey20); +.fx-relay-menu-dashboard-link:hover .fx-relay-menu-dashboard-link-tooltip, +.fx-relay-menu-dashboard-link:focus .fx-relay-menu-dashboard-link-tooltip, +.fx-relay-menu-dashboard-link:focus-visible .fx-relay-menu-dashboard-link-tooltip { + display: block; + margin: 0 var(--spacingXs); } -a.setting-label:focus { - box-shadow: var(--relayButtonFocus); -} +/* Panel Header */ -a.setting-label:active { - background-color: var(--relayGrey30); +.fx-relay-panel-header { + display: flex; + justify-content: center; + align-items: center; + width: 100%; } -.settings-list { - padding: 0; - margin: 0; - list-style-type: none; +.fx-relay-panel-header-btn-back { + position: absolute; + left: var(--spacingSm); + appearance: none; + background-color: transparent; + border: 0; margin: 0; + padding: var(--spacingXs); + cursor: pointer; } -.setting-link { - margin-bottom: 12px; +.fx-relay-panel-header-btn-back:focus, +.fx-relay-panel-header-btn-back:hover { + background-color: var(--colorGrey10); + border-radius: 100%; } -.setting-link:last-of-type { - margin-bottom: 0; +.fx-relay-panel-header-btn-back img { + display: block; + width: 24px; + height: 24px; + pointer-events: none; } -fx-relay-logo-wrapper { - background-color: white; +.fx-relay-panel-header-title { + font-size: var(--fontSizeBodyMd); + font-family: var(--relayMetropolis); + font-weight: 500; } -.settings-header, -fx-relay-logo-wrapper { +/* Sign Up/In Panel */ +sign-up-panel { + height: 100%; display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - padding: 0 24px 0 24px; - width: 100%; - min-height: 64px; - height: 64px; - max-height: 64px; -} - -.show-settings settings { - visibility: visible; - transform: translateX(0); - transition: all 0.3s ease; + flex-direction: column; + font-size: var(--fontSizeBodySm); + position: relative; } -.show-settings fx-relay-logo-wrapper, -.show-settings .signed-in-panel { - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; +sign-up-panel::after { + height: 3px; + background: var(--relayFxGradient); + content: ""; + position: absolute; + lefT: 0; + right: 0; + top: -1px; + background-size: 120%; } -.show-report-issue report-issue { - visibility: visible; - transform: translateX(0); - transition: all 0.3s ease; +.fx-relay-sign-in-copy { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + padding-top: var(--layoutSm); } -.show-report-issue fx-relay-logo-wrapper, -.show-report-issue settings { - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; +.fx-relay-sign-in-copy img { + margin-bottom: var(--spacingMd); } -fx-relay-logo-wrapper, -.signed-in-panel { - opacity: 1; - transition: all 0.5s ease; +.fx-relay-sign-in-copy h4 { + font-size: var(--fontSizeTitleXs); + font-weight: 700; + color: var(--colorGrey50); + margin: 0 0 var(--spacingMd); + padding: 0; } -.settings-hl { - margin: auto; - justify-content: center; - text-align: center; - font-size: 18px; - font-family: var(--relayMetropolis); - font-weight: 600; +.fx-relay-sign-in-copy p { + color: var(--colorGrey50); + max-width: var(--contentXs); + margin: 0 auto; } -.close-settings { - background-image: url("/images/arrowhead-left.svg"); - left: 24px; +.fx-relay-sign-in-button { + position: fixed; + bottom: 0; + width: 100%; + left: 0; + padding: var(--spacingMd); + background-color: var(--colorGrey05); } -.open-settings { - background-image: url("/images/preferences.svg"); - right: 24px; +/* Settings */ +#settings-panel { + font-size: var(--fontSizeBodyXs); } -.settings-report-issue { - cursor: pointer; +.fx-relay-settings-toggles { + padding: 0 var(--spacingMd); + background-color: var(--colorWhite); + border-radius: var(--borderRadiusSm); + display: flex; + flex-direction: column; } -.settings-toggle:hover, .settings-report-issue-return:hover { - opacity: .8; +.fx-relay-settings-toggle-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: var(--spacingMd) 0; } -.settings-toggle:focus, .settings-report-issue-return:focus { - box-shadow: var(--relayButtonFocus); +.fx-relay-settings-toggle-wrapper + +.fx-relay-settings-toggle-wrapper { + border-top: 1px solid var(--colorGrey10); } -.settings-toggle:active, .settings-report-issue-return:active { - opacity: 1; +.fx-relay-settings-toggle { + display: block; + height: 16px; + width: 28px; + min-width: 28px; + position: relative; + overflow: hidden; + border: none; + border-radius: 1.5em; + outline: none; + background-color: var(--colorGreen50); + background-size: 20px; + margin: 0; + cursor: pointer; } -.settings-toggle, .settings-report-issue-return { - border-radius: 50%; - background-repeat: no-repeat; - background-size: 18px; - background-position: center center; - height: 26px; - width: 26px; - position: absolute; - background-color: rgba(255, 255, 255, 0); - border: 1px solid rgba(255, 255, 255, 0); +.fx-relay-settings-toggle:checked:hover { + background-color: var(--colorGreen60); } -.intro { - margin: 0 auto 16px auto; - max-width: 260px; - color: var(--relayInkLight); - line-height: 1.3; - font-size: 15px; +.fx-relay-settings-toggle:checked:focus { + box-shadow: var(--colorGreen20); } -.hidden, -panel-content { - transition: all 0.2s ease; +.fx-relay-settings-toggle:checked:active { + background-color: var(--colorGreen70); } -panel-content { - display: flex; - flex-direction: column; +.fx-relay-settings-toggle::after { + content: ""; + height: 10px; + width: 10px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 1); position: absolute; top: 0; - right: 0; - left: 0; bottom: 0; - overflow: hidden; + margin: auto; + right: var(--spacingXs); + transition: all 0.2s ease; } -.button { - text-decoration: none; +.data-disabled::after, +.input-icons-disabled::after { + left: 4px; + right: 18px; + transition: all 0.2s ease; } -.welcome { - font-weight: 800; - font-size: 28px; - line-height: 1.2; - max-width: 260px; - text-align: center; - color: var(--relayInk); - font-family: var(--relayMetropolisBold); - margin: auto auto 8px auto; +.data-disabled:hover, +.data-disabled:focus, +.input-icons-disabled:hover, +.input-icons-disabled:focus { + background-color: var(--colorGrey40); } -.blue-primary-btn { - display: flex; - align-items: center; - justify-content: center; - background: var(--colorInformational); - color: var(--relayInk); - font-family: var(--relayMetropolis); - font-weight: 800; - font-size: 15px; - border: 0 none; - text-decoration: none; - color: rgba(255, 255, 255, 1); - min-width: 200px; - min-height: 48px; - margin: 0 auto auto auto; - padding: 12px 16px; - border-radius: 4px; +.data-disabled:active, +.input-icons-disabled:active { + background-color: var(--colorGrey50); } -.blue-primary-btn:hover { - background: var(--relayBlueHover); +.data-disabled, +.input-icons-disabled { + background-color: var(--colorGrey30); } -.blue-primary-btn:focus { - box-shadow: var(--relayButtonFocus); +.fx-relay-settings-links { + display: flex; + flex-direction: column; + gap: var(--spacingSm); + padding: var(--spacingMd) 0; } -.blue-primary-btn:active { - background: var(--relayBlueActive); +.fx-relay-settings-link { + background-color: transparent; + padding: var(--spacingSm) var(--spacingMd); + text-decoration: none; + color: var(--colorBlack); + cursor: pointer; + appearance: none; + border: 0; + text-align: left; + font-size: var(--fontSizeBodyXs); } -.survey-link:hover { - color: var(--relayBlueHover); +.fx-relay-settings-link:hover, +.fx-relay-settings-link:focus { + background-color: var(--colorViolet05); + border-radius: var(--borderRadiusSm); } -main-panel { +/* Report Panel */ + +.report-issue-content { display: flex; flex-direction: column; - height: 100%; + gap: var(--spacingMd); + padding: var(--spacingMd); + border: 0 none; + font-size: var(--fontSizeBodySm); } -.text-link { - font-weight: 700; +.report-section { + margin-bottom: var(--spacingMd); + display: flex; + gap: var(--spacingMd); + flex-direction: column; } -.panel-status { - display: flex; - justify-content: space-between; - padding: 8px 16px; - line-height: 150%; - font-size: 1rem; - border-bottom: 1px solid var(--relayGrey20); - border-top: 1px solid var(--relayGrey20); - width: 100%; - font-size: 13px; - font-family: var(--relayInterUiRegular); - margin-bottom: var(--spacingLg); +.report-label::after { + content: ":"; } -.aliases-remaining { +.report-section ul { + list-style-type: none; + padding: 0; margin: 0; - font-family: var(--relayInterUiRegular); - text-align: start; + display: flex; + flex-direction: column; + gap: var(--spacingMd); } -.premium-cta { +.report-section li { + display: flex; + align-items: flex-start; + padding: 0; margin: 0; - font-family: var(--relayInterUiRegular); - color: var(--colorInformational); - font-weight: 700; - text-decoration: none; + gap: var(--spacingSm); } -.premium-cta a { +.report-section input[type=checkbox]{ margin: 0; + padding: 0; + color: var(--colorGrey50); } -.num-aliases-remaining, -.max-num-aliases { - font-weight: 700; +.report-section input[type=checkbox]:checked + label { + color: var(--colorInformational); } -/*upgrade banner*/ - -.upgrade-banner-wrapper { - background: var(--relayGrey20); - border-radius: 8px; - padding: 10px; - margin-top: 8px; - display: flex; - text-decoration: none; +.report-section input[type=text]{ + width: 100%; + padding: var(--spacingSm) var(--spacing2xl) var(--spacingSm) var(--spacingMd); + border-radius: var(--borderRadiusSm); + background-color: var(--colorWhite); + border: 2px solid transparent; + outline: 1px solid var(--colorGrey30); } -.upgrade-banner-wrapper img { - width: 30px; - height: 30px; - margin: auto; +.report-section input[type=text]:focus{ + outline: 4px solid var(--colorInformationalFocus); + border: 2px solid var(--colorInformational); } -.upgrade-banner { - font-family: var(--relayInterUiRegular); - padding-left: 10px; - color: #5953F3; - margin: auto; - font-weight: 700; +.report-issue-content input[type=submit]:disabled{ + background: var(--colorInformationalDisabled); } -footer { +.report-success { + padding: var(--spacingMd); + padding-top: 0; display: flex; flex-direction: column; + justify-content: center; align-items: center; - margin: auto 0 0 0; - padding-top: var(--spacingMd); - border-top: 0.5px solid var(--colorGrey10); -} - -.footer-button { - width: 100%; - font-size: 13px; - padding-bottom: var(--spacingMd); - color: var(--colorInformational); - font-weight: 600; - text-decoration: none; - font-family: var(--relayInterUiRegular); text-align: center; + font-size: var(--fontSizeBodyMd); } -.footer-button:hover { - color: var(--colorInformationalHover) +.report-image-success { + max-width: 200px; } -.footer-button:active { - background-color: var(--relayGrey50); +.report-success h1 { + font-weight: 600; + font-size: var(--fontSizeBodyMd); } -.footer-button:focus { - color: var(--colorInformationalFocus) +.report-continue { + margin-top: var(--spacingLg); + cursor: pointer; + color: var(--colorInformational); + appearance: none; + background-color: transparent; + border: 0; } -/* onboarding */ +/* Stats */ +.dashboard-stats-list { + width: 100%; + border-radius: var(--borderRadiusSm); + padding: 0 var(--spacingLg); + font-size: var(--fontSizeBodySm); + background: var(--colorWhite); + margin-bottom: var(--spacingMd); +} -onboarding-panel { - font-size: 14px !important; - padding: 8px 30px 30px 30px; +.dashboard-stats-list ul { + list-style-type: none; + padding: 0; display: flex; - align-items: flex-start; - height: 100%; flex-direction: column; - position: relative; - transition: all 0.2s ease; - background: white; -} - -/* Premium Panel */ - -.premiumPanel { - padding: 8px 16px 0px 16px; - font-family: var(--relayInterUiRegular); -} - -.content-wrapper, .premium-wrapper { - display: flex; - width: 100%; - flex-direction: column; - gap: var(--spacingSm); -} - -.end-of-intro-pricing-wrapper { - display: flex; - height: 360px; - width: 100%; - flex-direction: column; - justify-content: space-around; -} - -.dashboard-stats-list { - width: 100%; - border-radius: 8px; - padding: 0 15px; - font-size: 13px; - background: #F7F7F7; -} - -.dashboard-stats-list ul { - list-style-type: none; - padding: 0; } .dashboard-stats-list li:first-child { font-weight: 600; - padding-bottom: 12px; -} - -.dashboard-stats-list li:nth-child(2) { - padding: 12px 0; -} - -.dashboard-stats-list li:nth-child(3) { - padding: 12px 0; + color: var(--colorBlack) } .dashboard-stats-list li:not(:first-child){ - border-top: 1px solid var(--relayGrey20); - padding-top: 12px; - padding-left: 30px; + padding-left: var(--spacingLg); + color: var(--colorGrey50) } .dashboard-info { display: flex; flex-direction: row; justify-content: space-between; + padding: var(--spacingMd) 0; +} + +.dashboard-info + .dashboard-info { + border-top: 1px solid var(--colorGrey10); } .dashboard-info::before { content: ""; + /* Custom size/positioning for icon on the stat row */ width: 20px; height: 20px; - margin-left: -25px; + margin-left: -25px; position: absolute; background-repeat: no-repeat; background-color: rgba(0, 0, 0, 0); transition: opacity 0.2s ease; } -.dashboard-info:nth-child(2)::before { +.dashboard-info-emails-blocked::before { background-image: url("/icons/blocked-icon.svg"); } -.dashboard-info:nth-child(3)::before { +.dashboard-info-emails-forwarded::before { background-image: url("/icons/forward-icon.svg"); } -.dashboard-info:nth-child(4)::before { +.dashboard-info-trackers-removed::before { background-image: url("/icons/email-trackers-icon.svg"); } @@ -549,452 +544,542 @@ onboarding-panel { align-items: right; } -.email-domain-illustration { - margin: 0 auto; - display: block; - height: 80px; -} +/* News */ -.register-domain-component { - margin: 0 auto; - padding-top: var(--spacingMd); +.fx-relay-news { display: flex; flex-direction: column; - height: auto; - gap: var(--spacingSm); + gap: var(--spacingXs); + list-style-type: none; + margin: 0; + padding: 0; + text-align: left; + align-items: flex-start; + justify-content: left; } -.register-domain-cta { - max-width: 200px; - font-size: 1rem; - padding: var(--spacingXs) var(--spacingSm); - text-align: center; +.fx-relay-news-item button { + appearance: none; + outline: none; + border: none; + background-color: transparent; + background-color: var(--colorWhite); + border-radius: var(--borderRadiusSm); + padding: var(--spacingMd); + cursor: pointer; + display: flex; + gap: var(--spacingMd); } -.register-domain-headline { - font-weight: 700; - font-size: 14px; - text-align: center; +.fx-relay-news-item button:focus, +.fx-relay-news-item button:hover { + background-color: var(--colorPurple05) } -.educational-component { - display: flex; - flex-direction: row; - padding: var(--spacingSm) 0; - justify-content: space-between; - height: auto; - gap: var(--spacingMd); - align-items: start; +.fx-relay-news-item-image { + pointer-events: none; + max-width: var(--layoutMd); + flex-shrink: 0; + flex-grow: 0; } -.education-description { - display: flex; - flex-direction: column; - flex: 2; +.fx-relay-news-item-image img { + display: block; width: 100%; } -.education-img-wrapper { - display: flex; - justify-content: center; - align-items: center; - flex: 1.5; - height: auto; - max-width: 120px; +.fx-relay-news-item-content { + text-align: left; + pointer-events: none; + width: 100%; } -#bundle-phones-promo #panel1 .education-img-wrapper, -#bundle-phones-promo #panel2 .education-img-wrapper - { - box-shadow: var(--boxShadowSm); - border-radius: var(--borderRadiusMd); - display: flex; - justify-content: center; - align-items: center; +.fx-relay-news-item-hero { + font-weight: 600; + font-size: var(--fontSizeBodyMd); + margin: 0 0 var(--spacingXs); + line-height: 20px; +} + +.fx-relay-news-item-body { + font-size: var(--fontSizeBodySm); + line-height: 20px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; - object-fit: cover; - height: auto; } -.education-img { - width: 100%; - height: auto; - justify-content: center; - align-items: center; - display: flex; +.fx-relay-news-story-link { + margin-top: var(--spacingMd); + color: var(--colorInformational); + display: block; + text-decoration: none; } -.education-headline { - font-family: var(--relayInterUiRegular); - font-weight: 700; - font-size: 15px; +.fx-relay-news-story-link:focus { + color: var(--colorInformationalFocus); } -.onboarding-h1, .educational-headline { - font-size: 15px; - font-family: var(--relayMetropolis); - margin: 0; +.fx-relay-news-story-link:hover { + color: var(--colorInformationalActive); } -onboarding-panel p, .education-body { - text-align: start; - margin: 0; - font-size: 12.5px; - line-height: 130%; +.fx-relay-news-story img { + display: block; + width: 100%; } -/* Add this class if description is too long */ -.small-font-size { - font-size: 11px; +/* Masks Panel */ + +.fx-relay-masks-available-count { + color: var(--colorGrey40); + font-size: var(--fontSizeBodySm); + display: inline-block; + margin-bottom: var(--spacingSm); } -.onboarding-cta { - cursor: pointer; - font-weight: 600; - font-size: 12.5px; - line-height: 170%; - text-decoration: underline; - color: var(--colorInformational); - align-self: flex-start; +.fx-relay-masks-limit-upgrade { + background-color: var(--colorErrorHover); + border-radius: var(--borderRadiusSm); + padding: var(--spacingSm); + color: var(--colorWhite); + font-size: var(--fontSizeBodyXs); + display: flex; + justify-content: center; + gap: var(--spacingSm); + margin-bottom: var(--spacingSm); } -.js-new-label { +.fx-relay-masks-error-message { + background-color: var(--colorError); + border-radius: var(--borderRadiusSm); + color: var(--colorWhite); + font-size: var(--fontSizeBodyXs); display: none; + margin-bottom: var(--spacingSm); + align-items: stretch; + padding: 0; + cursor: pointer; + overflow: hidden; } -.onboarding-cta:hover { - color: var(--colorInformationalHover); +.fx-relay-masks-error-message:hover { + background-color: var(--colorErrorHover); } -.onboarding-cta:focus { - color: var(--colorInformationalFocus); +.fx-relay-masks-error-message:active { + background-color: var(--colorErrorActive); } -.onboarding-img { - width: 100%; +.fx-relay-masks-error-message:focus .fx-relay-masks-error-message-icon { + background-color: var(--colorErrorHover); } -.img-wrapper { - max-height: 100%; - overflow: hidden; - text-align: center; - margin: 0; +.fx-relay-masks-error-message-string { + padding: var(--spacingSm) var(--spacingMd); } -.maxAliasesPanel .onboarding-pagination { - display: none !important; +.fx-relay-masks-error-message-icon { + width: 40px; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--colorError); } -.maxAliasesPanel .onboarding-img { - width: 200px; +.fx-relay-masks-error-message.is-shown { + display: flex; } -.survey-link { - color: var(--colorInformational); - margin: 12px auto 0 0; - border-radius: 2px; +.fx-relay-masks-error-message > * { + pointer-events: none; } -.onboarding-pagination { - position: absolute; +.fx-relay-no-masks-created { display: flex; - bottom: var(--spacingSm); + flex-direction: column; justify-content: center; - flex-direction: row; - left: 0; - right: 0; - margin: auto; + align-items: center; + text-align: center; + padding-top: var(--layoutLg); } -.panel-nav, .premium-panel-nav { - border: none; - outline: none; - border-radius: 50%; - width: 15px; - padding: 4px; - position: relative; - background-color: rgba(0, 0, 0, 0); +.fx-relay-no-masks-created img { + max-width: var(--contentXs); + margin-bottom: var(--spacingMd); } -.panel-nav::after, .premium-panel-nav::after { - content: ""; - display: inline-block; - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - background-position: center center; - background-size: 90%; - background-repeat: no-repeat; - background-image: url("/images/arrowhead-left.svg"); - opacity: .4; - transition: opacity 0.2s ease; +.fx-relay-no-masks-created h2 { + font-size: var(--fontSizeTitleXs); + font-weight: 700; + color: var(--colorGrey50); + margin: 0 auto var(--spacingMd); + max-width: var(--content2xs); } -.settings-toggle:hover::after, .settings-report-issue-return:hover::after, -.panel-nav:hover::after, .premium-panel-nav:hover::after { - opacity: .8; - transition: opacity 0.2s ease; +.fx-relay-no-masks-created p { + font-size: var(--fontSizeBodySm); + max-width: var(--contentXs); + padding: 0; + margin: 0 0 var(--spacingLg); } -.settings-toggle:focus, .settings-report-issue-return:focus, -.panel-nav:focus, .js-panel-nav:focus { - box-shadow: var(--relayButtonFocus); +.fx-relay-generate-mask-buttons { + border-top: 1px solid var(--colorGrey10); + position: fixed; + bottom: 0; + width: 100%; + left: 0; + padding: var(--spacingMd); + background-color: var(--colorGrey05); + display: flex; + gap: var(--spacingMd); + flex-direction: column; } -.settings-toggle:active::after, .settings-report-issue-return:active::after, -.panel-nav:active::after, .premium-panel-nav:active::after { - opacity: 1; - transition: opacity 0.2s ease; +/* Mask List/Item */ + +.fx-relay-mask-list { + list-style: none; + margin: 0; + padding: 0; } -.next-panel::after { - transform: rotate(180deg); +.fx-relay-mask-item { + display: flex; + flex-direction: column; + border-radius: var(--borderRadiusSm); + padding: var(--spacingSm); + position: relative; } -.panel-num, .js-panel-num { - width: auto; - margin: 0; - text-align: center; - justify-self: center; +.fx-relay-mask-item-new-mask-created { + /* opacity: 1; */ + opacity: 0; + pointer-events: none; + transition: opacity 2s; + background-color: var(--colorGreen50); + border-radius: var(--borderRadiusSm); + padding: var(--spacingXs) var(--spacingSm); + font-size: var(--fontSizeBodyXs); + font-weight: 500; + /* + By allowing this to overlap other elements, + we don't need to reserve empty space for it. + Otherwise, this empty space would push the + .expand-toggle out of the card on small screens: + */ + position: absolute; + left: var(--spacingSm); + top: 100%; + transform: translateY(-50%); + /* Z-index usage: raise element above other mask cards */ + z-index: 1; } -.current-panel, -.panel-num, .js-panel-num { - font-weight: 600; - font-size: 14px; - padding: 0 var(--spacingXs); - text-align: center; - display: inline; +.fx-relay-mask-item.is-new-mask .fx-relay-mask-item-new-mask-created { + pointer-events: auto; + opacity: 1; + /* Don't fade in when appearing: */ + transition: opacity 0s; } -.current-panel, .total-panels { - margin: 0 var(--spacingXs); - padding-right: var(--spacingXs); +.fx-relay-mask-item:focus-within, +.fx-relay-mask-item:focus, +.fx-relay-mask-item:hover { + background-color: var(--colorPurple05); } +.fx-relay-mask-item-label { + font-size: var(--fontSizeBodyXs); + color: var(--colorGrey40); + overflow: hidden; + display: block; + line-height: 1; +} +.fx-relay-mask-item-address-bar { + display: flex; + align-items: center; + gap: var(--spacingMd); +} -/* For mobile screens with a bigger height viewport than the browser popup. - This takes the --panelHeight + 1px as its min-height. */ +.fx-relay-mask-item-address-wrapper { + display: flex; + gap: var(--spacingXs); + flex-direction: column; + width: calc( 100% - var(--spacingMd)); + position: relative; + text-overflow: ellipsis; + overflow: hidden; +} -/* TODO: Understand why this exists before removing */ -/* @media screen and (min-height: 501px) { - .fx-relay-panel { - height: 100vh; - } +.fx-relay-mask-item-address { + user-select: all; + font-size: var(--fontSizeBodyMd); + font-weight: 600; + color: var(--colorGrey50); + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; +} - fx-relay-logo-wrapper { - margin-top: 2rem; - } +.fx-relay-mask-item-address::selection { + background: var(--colorPurple10); +} - fx-relay-logomark { - width: 48px; - height: 50px; - } +.fx-relay-mask-item-address-actions { + margin-left: auto; + display: flex; + gap: var(--spacingSm); + position: relative; +} - fx-relay-logotype { - width: 70%; - height: 50px; - } +.fx-relay-mask-item-address-actions > button { + appearance: none; + border: 0; + border-radius: 100%; + width: 32px; + height: 32px; + background-repeat: no-repeat; + background-position: center; + filter: grayscale(1); + cursor: pointer; +} - .intro { - margin-top: 5rem; - margin-bottom: 2rem; - } +.fx-relay-mask-item-address-actions > button:focus, +.fx-relay-mask-item-address-actions > button:hover { + background-color: white; + filter: grayscale(0); +} - .intro, - .alias, - .error-message { - font-size: 1rem; - } +.fx-relay-mask-item-address-copy { + background-image: url(/icons/nebula-copy-text.svg); +} - .text-link { - border-bottom: 1px solid var(--relayInk); - } +.fx-relay-mask-item-address-copy-success { + opacity: 0; + pointer-events: none; + transition: opacity 2s; + background-color: var(--colorGreen50); + border-radius: var(--borderRadiusSm); + padding: var(--spacingXs) var(--spacingSm); + font-size: var(--fontSizeBodyXs); + font-weight: 500; + /* + By allowing this to overlap other elements, + we don't need to reserve empty space for it. + Otherwise, this empty space would push the + .expand-toggle out of the card on small screens: + */ + position: absolute; + right: calc(100% + var(--spacingSm)); + top: 50%; + transform: translateY(-50%); +} - .create-account { - font-size: 1.1rem; - padding: 1rem 3rem; - } +.fx-relay-mask-item-address-copy-success.is-shown { + pointer-events: auto; + opacity: 1; + /* Don't fade in when appearing: */ + transition: opacity 0s; +} - alias-creation { - margin-top: 12rem; - } +.fx-relay-mask-item-address-toggle { + background-image: url(/icons/nebula-arrow-up-down.svg); +} - .view-dashboard { - margin: auto auto 0 auto; - } -} */ +/* Loading Bar */ -.newsflash-wrapper.is-hidden, .newsflash-wrapper .is-hidden { +/* Page Loader */ +.fx-relay-menu-loading-bar { + width: 100%; + height: 5px; + position: relative; + overflow: hidden; display: none; } -.newsflash-wrapper { - background-color: var(--relayGrey10); - padding: 8px; - margin-top: 16px; - height: 100%; +.fx-relay-menu-loading-bar-wrapper { + width: 100%; + height: 2px; + position: absolute; + left: 50%; + top: 1px; } -.newsflash-description { - padding: 0 8px; +.fx-relay-menu-loading-bar-border { + height: 100%; + width: 100%; + position: relative; + left: -50%; + top: -50%; + padding: 0; + background-color: var(--colorViolet20); + padding: 0; } -.newsflash-headline { - background: white; - font-size: 15px; - font-weight: bold; - padding: 12px 12px 12px 48px; - border-radius: 8px; - box-shadow: 0 5px 10px -3px rgba(29, 17, 51, .12); - font-family: var(--relayMetropolis); +.fx-relay-menu-loading-bar-whitespace { + overflow: hidden; + height: 100%; + width: 100%; + margin: 0 auto; + overflow: hidden; + position: relative; } -.newsflash-headline:before { - content: ""; - display: inline-block; - height: 20px; - width: 20px; - margin-left: -32px; - margin-top: -3px; - background-image: url("/images/icon-orange-info.svg"); - background-size: 20px; - background-repeat: no-repeat; - background-position: center center; +.fx-relay-menu-loading-bar-line { position: absolute; + height: 100%; + width: 60%; + background-color: var(--colorViolet70); + animation: cssload-slide 1s ease-in-out infinite; } -.newsflash-description a { - color: var(--colorInformational); - font-weight: bold; - padding-top: 8px; - text-decoration: none; +@keyframes cssload-slide { + 0% { + left: -100%; + } + + 100% { + left: 100%; + } } -.newsflash-wrapper:not(.has-approval-button) .newsflash-description a { +.is-loading .fx-relay-menu-loading-bar { display: block; - text-align: center; } -.newsflash-wrapper:not(.has-approval-button) .newsflash-description a:hover { - text-decoration: underline; + +.is-loading .fx-relay-menu { + min-height: 200px; } -.newsflash-body { - padding-top: 8px; +.is-loading .fx-relay-menu-content { + visibility: hidden; } -.newsflash-buttons { +/* Mask - Generate Custom Panel */ + + +.fx-relay-panel-custom-mask-form { display: flex; - margin-top: 48px; - justify-content: space-evenly; + flex-direction: column; + gap: var(--spacingMd); + padding: var(--spacingMd); + border: 0 none; + font-size: var(--fontSizeBodySm); } -.newsflash-buttons > button { - width: 120px; - padding: 8px 0; - font-size: 15px; - border-radius: 4px; - font-weight: bold; - text-align: center; - text-decoration: none; - border: 2px solid var(--colorInformational); - cursor: pointer; +.fx-relay-panel-custom-mask-input { + margin-bottom: var(--spacingMd); + display: flex; + gap: var(--spacingMd); + flex-direction: column; } -.newsflash-button-dismiss { - color: var(--colorInformational); - background: none; +.fx-relay-panel-custom-mask-checkbox { + display: flex; + align-items: flex-start; + margin-bottom: var(--spacingMd); + position: relative; } -.newsflash-button-allow { - background: var(--colorInformational); - color: white; +.fx-relay-panel-custom-mask-checkbox input[type=checkbox]{ + margin: 2px var(--spacingSm) 0 0; /* Custom Alignment Issue with Checkbox and Label */ + color: var(--colorGrey50); } -.newsflash-button-allow:hover, -.newsflash-button-dismiss:hover { - background: var(--relayBlueHover); - color: white; +.fx-relay-panel-custom-mask-checkbox input[type=checkbox]:checked + label { + color: var(--colorInformational); } -.newsflash-button-allow:focus, -.newsflash-button-dismiss:focus { - box-shadow: var(--relayButtonFocus); +.fx-relay-panel-custom-mask-input input[type=text]{ + width: 100%; + padding: var(--spacingSm) var(--spacing2xl) var(--spacingSm) var(--spacingMd); + border-radius: var(--borderRadiusSm); + background-color: var(--colorWhite); + border: 2px solid transparent; + outline: 1px solid var(--colorGrey30); } -.end-of-intro-pricing h1 { - font-size: 1.3em; +.fx-relay-panel-custom-mask-input-domain { + text-align: center; } -.end-of-intro-pricing p { - line-height: 120%; +.fx-relay-panel-custom-mask-input input[type=text]:focus{ + outline: 4px solid var(--colorInformationalFocus); + border: 2px solid var(--colorInformational); +} + +.fx-relay-panel-custom-mask-submit input[type=submit]:disabled{ + background: var(--colorInformationalDisabled); } -.countdown-timer { - align-self: center; +.fx-relay-panel-custom-mask-copy { + font-size: var(--fontSizeBodySm); + padding: 0 var(--spacingMd) var(--spacingMd); margin: 0; - font-family: var(--relayMetropolisBold); + border-bottom: 1px solid var(--colorGrey10); + overflow-wrap: break-word; + } -.countdown-timer dl { - display: flex; - gap: 8px; +.fx-relay-panel-custom-mask-promo-blocking-icon { + width: 16px; + height: 16px; + margin-left: var(--spacingSm); } -.countdown-timer dl > div { +.fx-relay-panel-custom-mask-promo-blocking-tooltip-wrapper:focus .fx-relay-panel-custom-mask-promo-blocking-tooltip, +.fx-relay-panel-custom-mask-promo-blocking-tooltip-wrapper:hover .fx-relay-panel-custom-mask-promo-blocking-tooltip { display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--spacingXs); - width: var(--layoutLg); - background-color: var(--colorViolet80); - border-radius: 8px; - color: white; - border: 2px solid var(--colorWhite); - box-shadow: 0px 5px 8px var(--colorGrey20); - padding: 8px; } -.countdown-timer dt { - font-weight: 600; +.fx-relay-panel-custom-mask-promo-blocking-tooltip { + display: none; + position: absolute; + left: 0; + bottom: calc(100%); + padding: var(--spacingMd); + box-shadow: 0px 0px 4px var(--colorGrey20); + /* Z-index: This box floats above the content to display informatin on what promo blocking is */ + z-index: 1; + background-color: var(--colorWhite); + border-radius: var(--borderRadiusSm); + flex-direction: column; + gap: var(--spacingSm); + /* This negative margin positions the promo blocking checkbox ABOVE the item */ + margin-top: calc(var(--spacingMd) * -1); } -.countdown-timer dd { - font-weight: 700; - font-size: 2.3em; +.fx-relay-panel-custom-mask-promo-blocking-tooltip h3 { + font-size: var(--fontSizeBodyMd); margin: 0; - font-family: var(--fontStackFirefoxBold); + padding: 0; } -.new-label { - background: var(--colorViolet70); - color: white; - font-size: 0.75em; - font-weight: 700; - align-self: flex-start; - text-transform: uppercase; - border-radius: var(--spacingXs); - padding: var(--spacingXs) var(--spacingSm); +.fx-relay-panel-custom-mask-promo-blocking-tooltip p { + font-size: var(--fontSizeBodySm); + margin: 0; + padding: 0; } -.hidden { - position: absolute; - opacity: 0; - visibility: hidden; - pointer-events: none; +.fx-relay-panel-custom-mask-promo-blocking-tooltip a { + font-size: var(--fontSizeBodySm); + margin: 0; + padding: 0; } -.is-hidden, .hidden { - position: absolute; - opacity: 0; - visibility: hidden; - pointer-events: none; +/* Search Bar */ + +.fx-relay-masks-search-form { + padding: var(--spacingXs) var(--spacingSm); + margin-bottom: var(--spacingSm); + position: relative; + display: none; } -.is-invisible { - opacity: 0; -} \ No newline at end of file +.fx-relay-masks-search-form.is-visible { + display: block; +} diff --git a/src/icons/nebula-arrow-up-down.svg b/src/icons/nebula-arrow-up-down.svg new file mode 100644 index 000000000..7ea4f96d4 --- /dev/null +++ b/src/icons/nebula-arrow-up-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-back-arrow.svg b/src/icons/nebula-back-arrow.svg new file mode 100644 index 000000000..285d42bc0 --- /dev/null +++ b/src/icons/nebula-back-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/nebula-close.svg b/src/icons/nebula-close.svg new file mode 100644 index 000000000..6241155c9 --- /dev/null +++ b/src/icons/nebula-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-copy-text.svg b/src/icons/nebula-copy-text.svg new file mode 100644 index 000000000..5d4d0f840 --- /dev/null +++ b/src/icons/nebula-copy-text.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-gear-purple.svg b/src/icons/nebula-gear-purple.svg new file mode 100644 index 000000000..9ae95dc8d --- /dev/null +++ b/src/icons/nebula-gear-purple.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-gear.svg b/src/icons/nebula-gear.svg new file mode 100644 index 000000000..8cecfb025 --- /dev/null +++ b/src/icons/nebula-gear.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-gift-purple.svg b/src/icons/nebula-gift-purple.svg new file mode 100644 index 000000000..fca7a66bc --- /dev/null +++ b/src/icons/nebula-gift-purple.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-gift.svg b/src/icons/nebula-gift.svg new file mode 100644 index 000000000..e5b6fa418 --- /dev/null +++ b/src/icons/nebula-gift.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-info.svg b/src/icons/nebula-info.svg new file mode 100644 index 000000000..86c2a5c96 --- /dev/null +++ b/src/icons/nebula-info.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-link-purple.svg b/src/icons/nebula-link-purple.svg new file mode 100644 index 000000000..aeffd3f3a --- /dev/null +++ b/src/icons/nebula-link-purple.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/nebula-link.svg b/src/icons/nebula-link.svg new file mode 100644 index 000000000..c09c277c6 --- /dev/null +++ b/src/icons/nebula-link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/nebula-stats-purple.svg b/src/icons/nebula-stats-purple.svg new file mode 100644 index 000000000..a17daecfc --- /dev/null +++ b/src/icons/nebula-stats-purple.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/nebula-stats.svg b/src/icons/nebula-stats.svg new file mode 100644 index 000000000..1b2f80d0b --- /dev/null +++ b/src/icons/nebula-stats.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/panel-images/announcements/panel-bundle-announcement-square.svg b/src/images/panel-images/announcements/panel-bundle-announcement-square.svg new file mode 100644 index 000000000..70d7488e3 --- /dev/null +++ b/src/images/panel-images/announcements/panel-bundle-announcement-square.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/panel-images/announcements/panel-bundle-announcement.svg b/src/images/panel-images/announcements/panel-bundle-announcement.svg index 28ddbd3f0..5c2deaabd 100644 --- a/src/images/panel-images/announcements/panel-bundle-announcement.svg +++ b/src/images/panel-images/announcements/panel-bundle-announcement.svg @@ -1,39 +1,39 @@ - - - - + + + + - - - - - + + + + + - - - + + + - + - - + + - - + + - + - - + + - - + + - + diff --git a/src/images/panel-images/announcements/premium-announcement-phone-masking-hero.svg b/src/images/panel-images/announcements/premium-announcement-phone-masking-hero.svg new file mode 100644 index 000000000..386c7bf41 --- /dev/null +++ b/src/images/panel-images/announcements/premium-announcement-phone-masking-hero.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/panel-images/announcements/premium-announcement-phone-masking.svg b/src/images/panel-images/announcements/premium-announcement-phone-masking.svg index 1f19086fa..da42a03be 100644 --- a/src/images/panel-images/announcements/premium-announcement-phone-masking.svg +++ b/src/images/panel-images/announcements/premium-announcement-phone-masking.svg @@ -1,77 +1,55 @@ - - - - + + + - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - - diff --git a/src/images/panel-images/man-sitting-email.svg b/src/images/panel-images/man-sitting-email.svg new file mode 100644 index 000000000..961a52099 --- /dev/null +++ b/src/images/panel-images/man-sitting-email.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/panel-images/report-submit-success.png b/src/images/panel-images/report-submit-success.png index d4ccfc9af..443a14f6e 100644 Binary files a/src/images/panel-images/report-submit-success.png and b/src/images/panel-images/report-submit-success.png differ diff --git a/src/js/background/background.js b/src/js/background/background.js index 6156a39f7..fbe599611 100644 --- a/src/js/background/background.js +++ b/src/js/background/background.js @@ -177,6 +177,18 @@ async function getCurrentPage() { return currentTab; } +async function getCurrentPageHostname() { + const currentPage = await getCurrentPage(); + + if (currentPage && currentPage.url) { + const url = new URL(currentPage.url); + return url.hostname; + } + + // Not a valid URL (about:// or chrome:// internal page) + return false; +} + // This function is defined as global in the ESLint config _because_ it is created here: // eslint-disable-next-line no-redeclare async function getServerStoragePref() { @@ -274,6 +286,73 @@ async function refreshAccountPages() { }); } +// This function is defined as global in the ESLint config _because_ it is created here: +// eslint-disable-next-line no-redeclare +async function makeDomainAddress(address, block_list_emails, description = null) { + const apiToken = await browser.storage.local.get("apiToken"); + + if (!apiToken.apiToken) { + browser.tabs.create({ + url: RELAY_SITE_ORIGIN, + }); + return; + } + + const { relayApiSource } = await browser.storage.local.get("relayApiSource"); + const serverStoragePermission = await getServerStoragePref(); + const relayApiUrlRelayAddress = `${relayApiSource}/domainaddresses/`; + + let apiBody = { + "enabled": true, + "description": "", + "block_list_emails": block_list_emails, + "used_on": "", + "address": address, + }; + + // Only send description/generated_for/used_on fields in the request if the user is opt'd into server storage + if (description && serverStoragePermission) { + apiBody.description = description; + apiBody.generated_for = description; + // The "," is appended here as this field is a comma-seperated list (but is a strict STRING type in the database). + // used_on lists all the different sites the add-on has populated a form field on for this mask + // Because it contains multiple websites, we're using the CSV structure to explode/filter the string later + apiBody.used_on = description + ","; + } + + + const headers = await createNewHeadersObject({auth: true}); + + const newRelayAddressResponse = await fetch(relayApiUrlRelayAddress, { + mode: "same-origin", + method: "POST", + headers: headers, + body: JSON.stringify(apiBody), + }); + + // Error Code Context: + // 400: Word not allowed (See https://github.com/mozilla/fx-private-relay/blob/main/emails/badwords.text) + // 402: Currently unknown. See FIXME in makeRelayAddress() function. + // 409: Custom mask name already exists + + if ([402, 409, 400].includes(newRelayAddressResponse.status)) { + return {status: newRelayAddressResponse.status}; + } + + let newRelayAddressJson = await newRelayAddressResponse.json(); + + if (description) { + newRelayAddressJson.description = description; + // Store the domain in which the alias was generated, separate from the label + newRelayAddressJson.generated_for = description; + } + + // Save the new mask in local storage + updateLocalStorageAddress(newRelayAddressJson); + + return newRelayAddressJson; +} + // This function is defined as global in the ESLint config _because_ it is created here: // eslint-disable-next-line no-redeclare async function makeRelayAddress(description = null) { @@ -301,6 +380,9 @@ async function makeRelayAddress(description = null) { if (description && serverStoragePermission) { apiBody.description = description; apiBody.generated_for = description; + // The "," is appended here as this field is a comma-seperated list (but is a strict STRING type in the database). + // used_on lists all the different sites the add-on has populated a form field on for this mask + // Because it contains multiple websites, we're using the CSV structure to explode/filter the string later apiBody.used_on = description + ","; } @@ -321,28 +403,36 @@ async function makeRelayAddress(description = null) { let newRelayAddressJson = await newRelayAddressResponse.json(); if (description) { - // TODO: Update the domain attribute to be "label" newRelayAddressJson.description = description; // Store the domain in which the alias was generated, separate from the label newRelayAddressJson.generated_for = description; } - // TODO: put this into an updateLocalAddresses() function + // Save the new mask in local storage + updateLocalStorageAddress(newRelayAddressJson); + + return newRelayAddressJson; +} + +async function updateLocalStorageAddress(newMaskJson) { const localStorageRelayAddresses = await browser.storage.local.get( "relayAddresses" ); + + // This is a storage function to save the newly created mask in the users local storage. + // We first confirm if there are addresses already saved, then add the new one to the list + // After adding it to the list, we re-sort the list by date created, ordering the newst masks to be listed first const localRelayAddresses = Object.keys(localStorageRelayAddresses).length === 0 ? { relayAddresses: [] } : localStorageRelayAddresses; const updatedLocalRelayAddresses = localRelayAddresses.relayAddresses.concat([ - newRelayAddressJson, + newMaskJson, ]); updatedLocalRelayAddresses.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)); - browser.storage.local.set({ relayAddresses: updatedLocalRelayAddresses }); - return newRelayAddressJson; + await browser.storage.local.set({ relayAddresses: updatedLocalRelayAddresses }); } async function updateAddOnAuthStatus(status) { @@ -387,7 +477,6 @@ async function displayBrowserActionBadge() { browser.runtime.onMessage.addListener(async (m, sender, _sendResponse) => { let response; - const currentPage = await getCurrentPage(); switch (m.method) { case "displayBrowserActionBadge": @@ -411,12 +500,12 @@ browser.runtime.onMessage.addListener(async (m, sender, _sendResponse) => { case "patchMaskInfo": await patchMaskInfo("PATCH", m.id, m.data, m.options); break; - case "getCurrentPage": - response = await getCurrentPage(); - break; case "getCurrentPageHostname": // Only capture the page hostanme if the active tab is an non-internal (about:) page. - if (currentPage.url) { response = (new URL(currentPage.url)).hostname } + response = await getCurrentPageHostname(); + break; + case "makeDomainAddress": + response = await makeDomainAddress(m.address, m.block_list_emails, m.description); break; case "makeRelayAddress": response = await makeRelayAddress(m.description); diff --git a/src/js/libs/psl.min.js b/src/js/libs/psl.min.js new file mode 100644 index 000000000..cbcd8eb3e --- /dev/null +++ b/src/js/libs/psl.min.js @@ -0,0 +1 @@ +!function(a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define([],a):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).psl=a()}(function(){return function e(s,n,t){function m(o,a){if(!n[o]){if(!s[o]){var i="function"==typeof require&&require;if(!a&&i)return i(o,!0);if(u)return u(o,!0);throw(a=new Error("Cannot find module '"+o+"'")).code="MODULE_NOT_FOUND",a}i=n[o]={exports:{}},s[o][0].call(i.exports,function(a){return m(s[o][1][a]||a)},i,i.exports,e,s,n,t)}return n[o].exports}for(var u="function"==typeof require&&require,a=0;a= 0x80 (not a basic code point)","invalid-input":"Invalid input"},l=j-1,y=Math.floor,f=String.fromCharCode;function v(a){throw new RangeError(c[a])}function k(a,o){for(var i=a.length,e=[];i--;)e[i]=o(a[i]);return e}function g(a,o){var i=a.split("@"),e="",i=(1>>10&1023|55296),a=56320|1023&a),o+=f(a)}).join("")}function z(a,o){return a+22+75*(a<26)-((0!=o)<<5)}function x(a,o,i){var e=0;for(a=i?y(a/m):a>>1,a+=y(a/o);l*b>>1y((d-p)/n))&&v("overflow"),p+=m*n,!(m<(m=t<=l?1:l+b<=t?b:t-l));t+=j)n>y(d/(m=j-m))&&v("overflow"),n*=m;l=x(p-s,o=u.length+1,0==s),y(p/o)>d-c&&v("overflow"),c+=y(p/o),p%=o,u.splice(p++,0,c)}return h(u)}function A(a){for(var o,i,e,s,n,t,m,u,r,p,c=[],l=(a=w(a)).length,k=128,g=72,h=o=0;hy((d-o)/(u=i+1))&&v("overflow"),o+=(s-k)*u,k=s,h=0;hd&&v("overflow"),m==k){for(n=o,t=j;!(n<(r=t<=g?1:g+b<=t?b:t-g));t+=j)c.push(f(z(r+(p=n-r)%(r=j-r),0))),n=y(p/r);c.push(f(z(n,0))),g=x(o,u,i==e),o=0,++i}++o,++k}return c.join("")}if(s={version:"1.4.1",ucs2:{decode:w,encode:h},decode:q,encode:A,toASCII:function(a){return g(a,function(a){return r.test(a)?"xn--"+A(a):a})},toUnicode:function(a){return g(a,function(a){return u.test(a)?q(a.slice(4).toLowerCase()):a})}},o&&i)if(_.exports==o)i.exports=s;else for(n in s)s.hasOwnProperty(n)&&(o[n]=s[n]);else a.punycode=s}.call(this)}.call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[2])(2)}); diff --git a/src/js/popup/popup.js b/src/js/popup/popup.js index c6448fe50..6fc470501 100644 --- a/src/js/popup/popup.js +++ b/src/js/popup/popup.js @@ -1,802 +1,1360 @@ +/* global getBrowser checkWaffleFlag psl */ -// eslint-disable-next-line no-unused-vars -async function checkWaffleFlag(flag) { - const waffleFlagArray = (await browser.storage.local.get("waffleFlags")).waffleFlags.WAFFLE_FLAGS; - for (let i of waffleFlagArray) { - if (i[0] === flag && i[1] === true) { - return true; - } - } - return false; -} - -// Announcements -async function getPromoPanels() { - const savings = "40%"; // For "Save 40%!" in the Bundle promo body - const getBundlePlans = (await browser.storage.local.get("bundlePlans")).bundlePlans.BUNDLE_PLANS; - const getBundlePrice = getBundlePlans.plan_country_lang_mapping[getBundlePlans.country_code].en.yearly.price; - const getBundleCurrency = getBundlePlans.plan_country_lang_mapping[getBundlePlans.country_code].en.yearly.currency - const userLocale = navigator.language; - const formattedBundlePrice = new Intl.NumberFormat(userLocale, { - style: "currency", - currency: getBundleCurrency, - }).format(getBundlePrice); - // Conditions for showing the Firefox password manager announcement - const isFirefoxIntegration = await checkWaffleFlag("firefox_integration"); - - const panels = { - "announcements": { - // Phone Masking Announcement - "panel1": { - "imgSrc": "announcements/panel-phone-masking-announcement.svg", - "imgSrcPremium": "announcements/premium-announcement-phone-masking.svg", - "tipHeadline": browser.i18n.getMessage("popupPhoneMaskingPromoHeadline"), - "longText": true, - "tipBody": browser.i18n.getMessage("popupPhoneMaskingPromoBody"), - "tipCta": browser.i18n.getMessage("popupPhoneMaskingPromoCTA"), - }, - "panel2": { - "imgSrc": "announcements/panel-bundle-announcement.svg", - "imgSrcPremium": "announcements/premium-announcement-bundle.svg", - "tipHeadline": browser.i18n.getMessage("popupBundlePromoHeadline_2", savings), - "tipBody": browser.i18n.getMessage("popupBundlePromoBody_3", formattedBundlePrice), - "tipCta": browser.i18n.getMessage("popupBundlePromoCTA"), - }, - }, - "premiumPanel": { - "aliasesUsedText": browser.i18n.getMessage("popupAliasesUsed_mask"), - "emailsBlockedText": browser.i18n.getMessage("popupEmailsBlocked"), - "emailsForwardedText": browser.i18n.getMessage("popupEmailsForwarded"), - } - } - - if (isFirefoxIntegration) { - panels.announcements["panel3"] = panels.announcements["panel1"] - panels.announcements["panel1"] = { - "imgSrc": "announcements/panel-announcement-password-manager-relay-illustration.svg", - "imgSrcPremium": "announcements/panel-announcement-password-manager-relay-square-illustration.svg", - "tipHeadline": browser.i18n.getMessage("popupPasswordManagerRelayHeadline"), - "tipBody": browser.i18n.getMessage("popupPasswordManagerRelayBody") - } - } - - return panels; -} - -async function getOnboardingPanels() { - // Conditions for showing the Firefox password manager announcement - const isFirefoxIntegration = await checkWaffleFlag("firefox_integration"); - - const panels = { - "announcements": { - "panel1": { - "imgSrc": "announcements/panel-announcement-attachment-limit.svg", - "tipHeadline": browser.i18n.getMessage("popupAttachmentSizeIncreaseHeadline"), - "tipBody": browser.i18n.getMessage("popupAttachmentSizeIncreaseBody"), - }, - "panel2": { - "imgSrc": "announcements/panel-announcement-critical-emails.svg", - "tipHeadline": browser.i18n.getMessage("popupBlockPromotionalEmailsHeadline_2"), - "tipBody": browser.i18n.getMessage("popupBlockPromotionalEmailsBodyNonPremium"), - }, - "panel3": { - "imgSrc": "announcements/panel-announcement-sign-back-in.svg", - "tipHeadline": browser.i18n.getMessage("popupSignBackInHeadline_mask"), - "tipBody": browser.i18n.getMessage("popupSignBackInBody_mask_v2"), - }, - }, - "maxAliasesPanel": { - "imgSrc": "high-five.svg", - "tipHeadline": browser.i18n.getMessage("popupOnboardingMaxAliasesPanelHeadline"), - "tipBody": browser.i18n.getMessage("popupOnboardingMaxAliasesPanelBody"), - "upgradeButton": browser.i18n.getMessage("popupUpgradeToPremiumBanner"), - "upgradeButtonIcon": "/icons/icon.svg", - }, - "premiumPanel": { - "aliasesUsedText": browser.i18n.getMessage("popupAliasesUsed_mask"), - "emailsBlockedText": browser.i18n.getMessage("popupEmailsBlocked"), - "emailsForwardedText": browser.i18n.getMessage("popupEmailsForwarded"), - } +(async () => { + // Global Data + const { relaySiteOrigin } = await browser.storage.local.get( + "relaySiteOrigin" + ); + + const sessionState = { + currentPanel: null, + newsItemsCount: null, + loggedIn: false, + newsContent: [] }; - if (isFirefoxIntegration) { - panels.announcements["panel4"] = panels.announcements["panel1"] - panels.announcements["panel1"] = { - "imgSrc": "announcements/panel-announcement-password-manager-relay-illustration.svg", - "tipHeadline": browser.i18n.getMessage("popupPasswordManagerRelayHeadline"), - "tipBody": browser.i18n.getMessage("popupPasswordManagerRelayBody"), - } - } - - return panels -} - -function getEducationalStrings() { - return { - "announcements": { - "panel1": { - "imgSrcPremium": "educational-matrix/educationalImg1.png", - "tipHeadline": browser.i18n.getMessage("popupEducationalComponent1Headline"), - "tipBody": browser.i18n.getMessage("popupEducationalComponent1Body"), + const popup = { + events: { + backClick: (e) => { + e.preventDefault(); + const backTarget = e.target.dataset.backTarget; + const backNavLevel = e.target.dataset.navLevel; + + if (backNavLevel === "root") { + document.querySelector(".js-internal-link.is-active")?.classList.remove("is-active"); + } + + // Custom rule to send "Closed Report Issue" event + if (e.target.dataset.navId && e.target.dataset.navId === "webcompat") { + sendRelayEvent("Panel", "click", "closed-report-issue"); + } + + // Catch back button clicks if the user is logged out + if (!sessionState.loggedIn && backNavLevel === "root") { + popup.panel.update("sign-up"); + return; + } + + popup.panel.update(backTarget); }, - "panel2": { - "imgSrcPremium": "educational-matrix/educationalImg-attachment-limit.svg", - "tipHeadline": browser.i18n.getMessage("popupAttachmentSizeIncreaseHeadline"), - "tipBody": browser.i18n.getMessage("popupAttachmentSizeIncreaseBody"), + dismissErrorClick: async (e) => { + e.preventDefault(); + e.target.classList.remove("is-shown"); }, - "panel3": { - "imgSrcPremium": "educational-matrix/educationalImg-block-emails.svg", - "tipHeadline": browser.i18n.getMessage("popupBlockPromotionalEmailsHeadline_2"), - "tipBody": browser.i18n.getMessage("popupBlockPromotionalEmailsBody_mask"), + externalClick: async (e) => { + e.preventDefault(); + if (e.target.dataset.eventLabel && e.target.dataset.eventAction) { + sendRelayEvent( + "Panel", + e.target.dataset.eventAction, + e.target.dataset.eventLabel + ); + } + await browser.tabs.create({ url: e.target.href }); + window.close(); }, - "panel4": { - "imgSrcPremium": "educational-matrix/educationalImg-sign-back-in.svg", - "tipHeadline": browser.i18n.getMessage("popupSignBackInHeadline_mask"), - "tipBody": browser.i18n.getMessage("popupSignBackInBody_mask_v2"), - "longText": true, + navigationClick: (e) => { + e.preventDefault(); + document + .querySelector(".js-internal-link.is-active") + ?.classList.remove("is-active"); + e.target.classList.add("is-active"); + const panelId = e.target.dataset.panelId; + popup.panel.update(panelId); + }, + generateMask: async (event, type = "random", data = null) => { + + // Types: "random", "custom" + sendRelayEvent("Panel", "click", `popup-generate-${type}-mask`); + preventDefaultBehavior(event); + + const isRandomMask = (type == "random"); + const isCustomMask = (type == "custom"); + const { premium } = await browser.storage.local.get("premium"); + + event.target.classList.add("is-loading"); + + const newRelayAddressResponseArgs = isCustomMask ? { method: "makeDomainAddress" } : { method: "makeRelayAddress" } + + if (isRandomMask) { + // When rebuilding panel, scroll to the top of it + const panel = document.querySelector(".fx-relay-mask-list"); + panel.scrollIntoView(true); + } + + // Request the active tab from the background script and parse the `document.location.hostname` + const currentPageHostName = await browser.runtime.sendMessage({ + method: "getCurrentPageHostname", + }); + + // If active tab is a non-internal browser page, add a label to the creation request + if (currentPageHostName !== null) { + newRelayAddressResponseArgs.description = currentPageHostName; + } + + if (isCustomMask && data) { + newRelayAddressResponseArgs.address = data.address + newRelayAddressResponseArgs.block_list_emails = data.block_list_emails + } + + // Attempt to create a new alias + const newRelayAddressResponse = await browser.runtime.sendMessage(newRelayAddressResponseArgs); + + // Catch edge cases where the "Generate New Alias" button is still enabled, + // but the user has already reached the max number of aliases. + if (newRelayAddressResponse.status === 402) { + event.target.classList.remove("is-loading"); + throw new Error( + browser.i18n.getMessage("pageInputIconMaxAliasesError_mask") + ); + } + + // Reset previous form + if (premium && isCustomMask) { + const customMaskDomainInput = document.getElementById("customMaskName"); + customMaskDomainInput.value = ""; + const customMaskBlockPromosCheckbox = document.getElementById("customMaskBlockPromos"); + customMaskBlockPromosCheckbox.checked = false; + } + + // Catch edge cases where the "Generate New Alias" button is still enabled, + // but the user has already reached the max number of aliases. + if (newRelayAddressResponse.status === 409 || newRelayAddressResponse.status === 400) { + event.target.classList.remove("is-loading"); + + const errorMessage = document.querySelector(".fx-relay-masks-error-message"); + errorMessage.classList.add("is-shown"); + + errorMessage.addEventListener("click",popup.events.dismissErrorClick, false); + + await popup.panel.masks.utilities.buildMasksList({newMaskCreated: false}); + + return; + } + + event.target.classList.remove("is-loading"); + + // Hide onboarding panel + const noMasksCreatedPanel = document.querySelector(".fx-relay-no-masks-created"); + noMasksCreatedPanel.classList.add("is-hidden"); + + await popup.panel.masks.utilities.buildMasksList({newMaskCreated: true}); + + + if (!premium) { + await popup.panel.masks.utilities.setRemainingMaskCount(); + } + } - } - }; -} - -function showSignUpPanel() { - const signUpOrInPanel = document.querySelector(".sign-up-panel"); - document.body.classList.add("sign-up"); - return signUpOrInPanel.classList.remove("hidden"); -} - -const serverStoragePanel = { - isRelevant: async () => { - const { serverStoragePrompt } = await browser.storage.local.get( - "serverStoragePrompt" - ); - - const serverStoragePref = await browser.runtime.sendMessage({ - method: "getServerStoragePref" - }); - - // TODO: Check when user was created - - // Only show the server prompt panel the user has not already opt'd in, - // or if they have not interacted with the panel before. - if (!serverStoragePref && !serverStoragePrompt) { - return true; - } - - return false; - }, - hide: () => { - const serverStoragePanelWrapper = document.querySelector( - ".js-server-storage-wrapper" - ); - - document.querySelectorAll(".content-wrapper").forEach((div) => { - div.classList.remove("is-hidden"); - }); - - serverStoragePanelWrapper.classList.add("is-hidden"); - serverStoragePanelWrapper - .querySelectorAll(".is-hidden") - .forEach((childDiv) => childDiv.classList.add("is-hidden")); - }, - init: (premium) => { - // Server Storage Prompt Panel - const serverStoragePanelWrapper = document.querySelector( - ".js-server-storage-wrapper" - ); - - if (premium) { - const panelStatus = document.querySelector(".panel-status"); - panelStatus.classList.add("is-hidden"); - } - - document.querySelectorAll(".content-wrapper").forEach((div) => { - div.classList.add("is-hidden"); - }); - - serverStoragePanelWrapper.classList.remove("is-hidden"); - - serverStoragePanelWrapper - .querySelectorAll(".is-hidden") - .forEach((childDiv) => childDiv.classList.remove("is-hidden")); - - const serverStoragePanelButtonDismiss = - serverStoragePanelWrapper.querySelector(".js-button-dismiss"); - - const serverStoragePanelButtonAllow = - serverStoragePanelWrapper.querySelector(".js-button-allow"); - - serverStoragePanelButtonDismiss.addEventListener( - "click", - serverStoragePanel.event.dismiss, - false - ); - - serverStoragePanelButtonAllow.addEventListener( - "click", - serverStoragePanel.event.allow, - false - ); - }, - event: { - dismiss: async (e) => { - e.preventDefault(); - serverStoragePanel.event.dontShowPanelAgain(); - serverStoragePanel.hide(); - showRelayPanel(1); }, + init: async () => { + // Set Navigation Listeners + const navigationButtons = document.querySelectorAll(".js-internal-link"); + navigationButtons.forEach((button) => { + button.addEventListener("click", popup.events.navigationClick, false); + }); - allow: async (e) => { - e.preventDefault(); - - const { relaySiteOrigin } = await browser.storage.local.get( - "relaySiteOrigin" + // Set Back Button Listeners + const backButtons = document.querySelectorAll( + ".fx-relay-panel-header-btn-back" ); - - serverStoragePanel.event.dontShowPanelAgain(); - - browser.tabs.create({ - url: `${relaySiteOrigin}/accounts/profile/?utm_source=fx-relay-addon&utm_medium=popup&utm_content=allow-labels-sync#sync-labels`, - active: true, + + backButtons.forEach((button) => { + button.addEventListener("click", popup.events.backClick, false); }); - window.close(); - }, - - dontShowPanelAgain: () => { - browser.storage.local.set({ serverStoragePrompt: true }); - } - }, -}; - -async function choosePanel(panelId, premium, premiumSubdomainSet) { - const premiumPanelWrapper = document.querySelector(".premium-wrapper"); - - if (premium) { - document.getElementsByClassName("content-wrapper")[0].remove(); - premiumPanelWrapper.classList.remove("is-hidden"); - //Toggle register domain or education module - checkUserSubdomain(premiumSubdomainSet); - return "premiumPanel"; - } else { - const premiumWrapper = document.getElementsByClassName("premium-wrapper"); - if (premiumWrapper.length) { - premiumWrapper[0].remove(); - } - - return `panel${panelId}`; - } -} - -function checkUserSubdomain(premiumSubdomainSet) { - const educationalComponent = document.querySelector(".educational-component"); - const registerDomainComponent = document.querySelector(".register-domain-component"); - - if (premiumSubdomainSet !== "None") { - registerDomainComponent.classList.add("is-hidden"); - } - - else { - educationalComponent.classList.add("is-hidden"); - } -} - - -async function showRelayPanel(tipPanelToShow) { - const onboardingPanelWrapper = document.querySelector("onboarding-panel"); - const tipImageEl = onboardingPanelWrapper.querySelector("img"); - const tipHeadlineEl = onboardingPanelWrapper.querySelector(".onboarding-h1"); - const tipBodyEl = onboardingPanelWrapper.querySelector(".onboarding-p"); - const currentPanel = onboardingPanelWrapper.querySelector(".current-panel"); - const upgradeButtonEl = onboardingPanelWrapper.querySelector(".upgrade-banner"); - const upgradeButtonIconEl = onboardingPanelWrapper.querySelector(".upgrade-banner-icon"); - const promoElements = onboardingPanelWrapper.querySelectorAll(".js-promo-item"); - const tipCtaEl = onboardingPanelWrapper.querySelector(".onboarding-cta"); - let premiumPanelStrings = getEducationalStrings(); - let onboardingPanelStrings = await getOnboardingPanels(); - - const isBundleAvailableInCountry = (await browser.storage.local.get("bundlePlans")).bundlePlans.BUNDLE_PLANS.available_in_country; - const isPhoneAvailableInCountry = (await browser.storage.local.get("phonePlans")).phonePlans.PHONE_PLANS.available_in_country; - - // Conditions for phone masking announcement to be shown: if the user is in US/CAN, phone flag is on, and user has not purchased phone plan yet - const isPhoneMaskingAvailable = await checkWaffleFlag("phones") && isPhoneAvailableInCountry; - // Conditions for bundle announcement to be shown: if the user is in US/CAN, bundle flag is on, and user has not purchased bundle plan yet - const isBundleAvailable = await checkWaffleFlag("bundle") && isBundleAvailableInCountry; - - if ( - isPhoneMaskingAvailable || isBundleAvailable - ) { - promoElements.forEach(i => { - i.classList.remove("is-hidden"); - }); - onboardingPanelWrapper.setAttribute("id", "bundle-phones-promo"); - - // If phone masking / bundle is available, switch panels to promo panel set to advertise phone masking/bundle plans - onboardingPanelStrings = await getPromoPanels(); - premiumPanelStrings = await getPromoPanels(); - } - - if (!browser.menus) { - // Remove sign back in for browsers that don't support menus API (Chrome) - delete onboardingPanelStrings.announcements.panel3; - delete premiumPanelStrings.announcements.panel4; - } - - //Premium Panel - const premiumPanelWrapper = document.querySelector(".premium-wrapper"); - const registerDomainImgEl = premiumPanelWrapper.querySelector(".email-domain-illustration"); - - //Dashboard Statistics - const dashboardStatistics = document.querySelectorAll(".dashboard-stats-list"); - - //Get profile data from site - const { aliasesUsedVal } = await browser.storage.local.get("aliasesUsedVal"); - const { emailsForwardedVal } = await browser.storage.local.get("emailsForwardedVal"); - const { emailsBlockedVal } = await browser.storage.local.get("emailsBlockedVal"); - const { emailTrackersRemovedVal } = await browser.storage.local.get("emailTrackersRemovedVal"); - - dashboardStatistics.forEach((statSet) => { - const aliasesUsedValEl = statSet.querySelector(".aliases-used"); - const emailsBlockedValEl = statSet.querySelector(".emails-blocked"); - const emailsForwardedValEl = statSet.querySelector(".emails-forwarded"); - const emailTrackersRemovedValEl = statSet.querySelector(".email-trackers-removed"); - - aliasesUsedValEl.textContent = aliasesUsedVal; - emailsBlockedValEl.textContent = emailsBlockedVal; - emailsForwardedValEl.textContent = emailsForwardedVal; - emailTrackersRemovedValEl.textContent = emailTrackersRemovedVal; - }); - - //Check if premium features are available - const premiumCountryAvailability = (await browser.storage.local.get("periodicalPremiumPlans")).periodicalPremiumPlans?.PERIODICAL_PREMIUM_PLANS - - //Check if user is premium - const { premium } = await browser.storage.local.get("premium"); - - //Check if user has a subdomain set - const { premiumSubdomainSet } = await browser.storage.local.get("premiumSubdomainSet"); - - //Educational Panel - const educationalImgEl = premiumPanelWrapper.querySelector(".education-img"); - const educationHeadlineEl = premiumPanelWrapper.querySelector(".education-headline"); - const educationBodyEl = premiumPanelWrapper.querySelector(".education-body"); - const currentEducationalPanel = premiumPanelWrapper.querySelector(".current-panel"); - const educationalCtaEl = premiumPanelWrapper.querySelector(".onboarding-cta"); - - const updatePremiumPanel = async (panelId) => { - const panelToShow = `panel${panelId}`; - premiumPanelWrapper.setAttribute("id", panelToShow); - let panelStrings = premiumPanelStrings.announcements[`${panelToShow}`]; - - if (!panelStrings) { - // Exit early if on a non-onboarding - return; - } - educationBodyEl.classList.remove("small-font-size"); - if (panelStrings.longText) { - educationBodyEl.classList.add("small-font-size"); - } - - // If bundle is unavailable but phone masking is, only show the phone masking promo - if (!isBundleAvailable && isPhoneMaskingAvailable) { - delete premiumPanelStrings.announcements.panel2; - } - - // If phone masking is unavailable but bundle is, only show bundle promo - if (!isPhoneMaskingAvailable && isBundleAvailable) { - // Force panel to start at panel2, which is the bundle promo - panelStrings = premiumPanelStrings.announcements.panel2; - delete premiumPanelStrings.announcements.panel1; - } - - setPagination(panelId); - - educationHeadlineEl.textContent = panelStrings.tipHeadline; - educationBodyEl.textContent = panelStrings.tipBody; - educationalImgEl.src = `/images/panel-images/${panelStrings.imgSrcPremium}`; - educationalCtaEl.textContent = panelStrings.tipCta; - currentEducationalPanel.textContent = `${tipPanelToShow}`; - - registerDomainImgEl.src = `/images/panel-images/email-domain-illustration.svg`; - - // Remove panel status if user has unlimited aliases, so no negative alias left count - if (premium) { - const panelStatus = document.querySelector(".panel-status"); - panelStatus.classList.add("is-hidden"); - } - - return; - }; - - const updatePanel = async (numRemaining, panelId) => { - const panelToShow = await choosePanel(panelId, premium, premiumSubdomainSet); - onboardingPanelWrapper.classList = [panelToShow]; - - let panelStrings = onboardingPanelStrings.announcements[`${panelToShow}`]; - - // If bundle is unavailable but phone masking is, only show the phone masking promo - if (!isBundleAvailable && isPhoneMaskingAvailable) { - delete onboardingPanelStrings.announcements.panel2; - } - - // If phone masking is unavailable but bundle is, only show bundle promo - if (!isPhoneMaskingAvailable && isBundleAvailable) { - // Force panel to start at panel2, which is the bundle promo - panelStrings = onboardingPanelStrings.announcements.panel2; - delete onboardingPanelStrings.announcements.panel1; - } - - setPagination(panelId); - - // Only show maxAliasesPanel to users where bundle / phone masking is unavailable - // Otherwise, show Phone masking and Bundle promo - if (!premium && numRemaining === 0 && !(isPhoneMaskingAvailable || isBundleAvailable)) { - panelStrings = onboardingPanelStrings["maxAliasesPanel"]; - onboardingPanelWrapper.classList = "maxAliasesPanel"; - - if (premiumCountryAvailability?.available_in_country === true) { - const upgradeButton = document.querySelector(".upgrade-banner-wrapper"); - upgradeButton.classList.remove("is-hidden"); - } - } - if (!panelStrings) { - // Exit early if on a non-onboarding - return; - } - - tipImageEl.src = `/images/panel-images/${panelStrings.imgSrc}`; - tipHeadlineEl.textContent = panelStrings.tipHeadline; - tipBodyEl.textContent = panelStrings.tipBody; - tipCtaEl.textContent = panelStrings.tipCta; - currentPanel.textContent = `${panelId}`; - upgradeButtonEl.textContent = panelStrings.upgradeButton; - upgradeButtonIconEl.src = panelStrings.upgradeButtonIcon; - - //If Premium features are not available, do not show upgrade CTA on the panel - if (premiumCountryAvailability?.available_in_country === true) { - const premiumCTA = document.querySelector(".premium-cta"); - premiumCTA.classList.remove("is-hidden"); - } - - return; - }; - - const setPagination = (activePanel) => { - const pagination = onboardingPanelWrapper.querySelector(".onboarding-pagination"); - const prevButton = onboardingPanelWrapper.querySelector(".previous-panel"); - const nextButton = onboardingPanelWrapper.querySelector(".next-panel"); - const totalPanelsEl = document.querySelector(".total-panels"); - // Number of panels available for free users - let totalPanels = Object.keys(onboardingPanelStrings.announcements).length; - if (premium) { - totalPanels = Object.keys(premiumPanelStrings.announcements).length; - } - totalPanelsEl.textContent = totalPanels; - prevButton.classList.remove("is-invisible"); - nextButton.classList.remove("is-invisible"); - // If user is at the start of the carousel, hide next button - if (activePanel === 1) { - prevButton.classList.add("is-invisible"); - } - // If user is at the end of the carousel, hide next button - if (activePanel === totalPanels) { - nextButton.classList.add("is-invisible"); - } - if (totalPanels === 1) { - pagination.classList.add("is-hidden"); - } - } - - //Nonpremium panel status - const { relayAddresses, maxNumAliases } = await getRemainingAliases(); - const numRemaining = maxNumAliases - relayAddresses.length; - const remainingAliasMessage = document.querySelector(".aliases-remaining"); - const getUnlimitedAliases = document.querySelector(".premium-cta"); - getUnlimitedAliases.textContent = browser.i18n.getMessage("popupGetUnlimitedAliases_mask"); - document.body.classList.add("relay-panel"); - - // Prevent negative masks from showing, default to 0 if all free masks have been used up - // TODO: Create re-usable data fetching and caching method for data syncing - let numRemainingNonNegative = numRemaining; - if (numRemaining <= 0) { - numRemainingNonNegative = 0; - } - remainingAliasMessage.textContent = browser.i18n.getMessage("popupRemainingAliases_2_mask", [numRemainingNonNegative, maxNumAliases]); - - updatePremiumPanel(tipPanelToShow); - updatePanel(numRemaining, tipPanelToShow); - - document.querySelectorAll(".panel-nav").forEach(navBtn => { - navBtn.addEventListener("click", () => { - sendRelayEvent("Panel", "click", "panel-navigation-arrow"); - // pointer events are disabled in popup CSS for the "previous" button on panel 1 - // and the "next" button on panel 3 - const nextPanel = (navBtn.dataset.direction === "-1") ? -1 : 1; - return updatePanel(numRemaining, tipPanelToShow += nextPanel); - }); - }); - - document.querySelectorAll(".premium-panel-nav").forEach(navBtn => { - navBtn.addEventListener("click", () => { - sendRelayEvent("Panel", "click", "panel-navigation-arrow"); - // pointer events are disabled in popup CSS for the "previous" button on panel 1 - // and the "next" button on panel 3 - const nextPanel = (navBtn.dataset.direction === "-1") ? -1 : 1; - return updatePremiumPanel(tipPanelToShow += nextPanel); - }); - }); - - if (premium) { - remainingAliasMessage.classList.add("is-hidden"); - } - - if (premiumCountryAvailability?.available_in_country === true) { - getUnlimitedAliases.classList.remove("is-hidden"); - } - - const relayPanel = document.querySelector(".signed-in-panel"); - relayPanel.classList.remove("hidden"); - - if (numRemaining === 0) { - return sendRelayEvent("Panel", "viewed-panel", "panel-max-aliases"); - } - return sendRelayEvent("Panel", "viewed-panel", "authenticated-user-panel"); -} - - -async function getAllAliases() { - return await browser.storage.local.get("relayAddresses"); -} - - -async function getRemainingAliases() { - const { relayAddresses } = await getAllAliases(); - const { maxNumAliases } = await browser.storage.local.get("maxNumAliases"); - return { relayAddresses, maxNumAliases }; -} - -async function getBrowser() { - if (typeof browser.runtime.getBrowserInfo === "function") { - /** @type {{ name: string, vendor: string, version: string, buildID: string }} */ - const browserInfo = await browser.runtime.getBrowserInfo(); - return browserInfo.name; - } - if (navigator.userAgent.toLowerCase().indexOf("firefox") !== -1) { - return "Firefox"; - } - return "Chrome"; -} - -async function enableSettingsPanel() { - const settingsToggles = document.querySelectorAll(".settings-toggle"); - settingsToggles.forEach(toggle => { - toggle.addEventListener("click", () => { - document.body.classList.toggle("show-settings"); - const eventLabel = document.body.classList.contains("show-settings") ? "opened-settings" : "closed-settings"; - if (document.body.classList.contains("show-settings")) { - sendRelayEvent("Panel", "click", eventLabel); - } - }); - }); - - const currentBrowser = await getBrowser(); - - if (currentBrowser === "Chrome") { - const supportLink = document.getElementById("popupSettingsLeaveFeedbackLink"); - const chromeSupportLink = "https://chrome.google.com/webstore/detail/firefox-relay/lknpoadjjkjcmjhbjpcljdednccbldeb/?utm_source=fx-relay-addon&utm_medium=popup" - supportLink.href = chromeSupportLink; - } -} - -function enableReportIssuePanel() { - const reportIssueToggle = document.querySelector(".settings-report-issue"); - const reportIssueSettingsReturn = document.querySelector(".settings-report-issue-return"); - const submissionSuccessContinue = document.querySelector(".report-continue"); - [reportIssueToggle, reportIssueSettingsReturn, submissionSuccessContinue].forEach(e => { - e.addEventListener("click", () => { - document.body.classList.toggle("show-report-issue"); - const eventLabel = document.body.classList.contains("show-report-issue") ? "opened-report-issue" : "closed-report-issue"; - if (document.body.classList.contains("show-report-issue")) { - sendRelayEvent("Panel", "click", eventLabel); - } - }); - }); - const reportForm = document.querySelector(".report-issue-content"); - setURLwithIssue(); - showReportInputOtherTextField(); - showSuccessReportSubmission(); - reportForm.addEventListener('submit', handleReportIssueFormSubmission); -} - -async function handleReportIssueFormSubmission(event) { - event.preventDefault(); - const data = new FormData(event.target); - const reportData = Object.fromEntries(data.entries()); - reportData.user_agent = await getBrowser(); - - Object.keys(reportData).forEach(function(value) { - // Switch "on" to true - if (reportData[value] === "on") { - reportData[value] = true; - } - // Remove from report if empty string - if (reportData[value] === "") { - delete reportData[value]; - } - }); - // Clean URL data to add "http://" before it if the custom input doesn't contain a HTTP protocol - if (!(reportData.issue_on_domain.startsWith("http://") || reportData.issue_on_domain.startsWith("https://"))) { - reportData.issue_on_domain = "http://" + reportData.issue_on_domain; - } - await browser.runtime.sendMessage({ - method: "postReportWebcompatIssue", - description: reportData - }); -} - -function showSuccessReportSubmission() { - const reportIssueSubmitBtn = document.querySelector(".report-issue-submit-btn"); - const reportSuccess = document.querySelector(".report-success"); - const reportContent = document.querySelector(".report-issue-content"); - reportIssueSubmitBtn.addEventListener("click", () => { - reportSuccess.classList.remove("is-hidden"); - reportContent.classList.add("is-hidden"); - }); -} - -function isSortaAURL(str) { - return str.includes(".") && !str.endsWith(".") && !str.startsWith("."); -} - -async function setURLwithIssue() { - // Add Site URL placeholder - const currentPage = (await getCurrentPage()).url; - const reportIssueSubmitBtn = document.querySelector(".report-issue-submit-btn"); - const inputFieldUrl = document.querySelector('input[name="issue_on_domain"]'); - reportIssueSubmitBtn.disabled = true; - - // Allow for custom URL inputs - inputFieldUrl.addEventListener('input', () => { - reportIssueSubmitBtn.disabled = true; - // Ensure that the custom input looks like a URL without https:// or http:// (e.g. test.com, www.test.com) - if (isSortaAURL(inputFieldUrl.value)) { - reportIssueSubmitBtn.disabled = false; - } - }); - - // Check that the host site has a valid URL - if (currentPage) { - const url = new URL(currentPage); - // returns a http:// or https:// value - inputFieldUrl.value = url.origin; - reportIssueSubmitBtn.disabled = false; - } -} - - function showReportInputOtherTextField() { - const otherCheckbox = document.querySelector('input[name="issue-case-other"]'); - const otherTextField = document.querySelector('input[name="other_issue"]'); - otherCheckbox.addEventListener("click", () => { - otherTextField.classList.toggle("is-hidden"); - }) - - // Add placeholder to report input on 'Other' selection - const inputFieldOtherDetails = document.querySelector('input[name="other_issue"]'); - inputFieldOtherDetails.placeholder = browser.i18n.getMessage("popupReportIssueCaseOtherDetails"); -} - -async function getCurrentPage() { - const [currentTab] = await browser.tabs.query({ - active: true, - currentWindow: true, - }); - return currentTab; -} - -async function enableInputIconDisabling() { - const inputIconVisibilityToggle = document.querySelector(".toggle-icon-in-page-visibility"); - - const stylePrefToggle = (inputsEnabled) => { - if (inputsEnabled === "show-input-icons") { - inputIconVisibilityToggle.dataset.iconVisibilityOption = "disable-input-icon"; - inputIconVisibilityToggle.classList.remove("input-icons-disabled"); - return; - } - inputIconVisibilityToggle.dataset.iconVisibilityOption = "enable-input-icon"; - inputIconVisibilityToggle.classList.add("input-icons-disabled"); - }; - - - const iconsAreEnabled = await areInputIconsEnabled(); - const userIconChoice = iconsAreEnabled ? "show-input-icons" : "hide-input-icons"; - stylePrefToggle(userIconChoice); - - inputIconVisibilityToggle.addEventListener("click", async () => { - const userIconPreference = (inputIconVisibilityToggle.dataset.iconVisibilityOption === "disable-input-icon") ? "hide-input-icons" : "show-input-icons"; - await browser.runtime.sendMessage({ - method: "updateInputIconPref", - iconPref: userIconPreference, - }); - sendRelayEvent("Panel", "click", userIconPreference); - return stylePrefToggle(userIconPreference); - }); - -} - -async function clearBrowserActionBadge() { - const { browserActionBadgesClicked } = await browser.storage.local.get( - "browserActionBadgesClicked" - ); - - // Dismiss the browserActionBadge only when it exists - if (browserActionBadgesClicked === false) { - browser.storage.local.set({ browserActionBadgesClicked: true }); - browser.browserAction.setBadgeBackgroundColor({ color: null }); - browser.browserAction.setBadgeText({ text: "" }); - } -} - -async function popup() { - sendRelayEvent("Panel", "opened-panel", "any-panel"); - clearBrowserActionBadge(); - const userApiToken = await browser.storage.local.get("apiToken"); - const signedInUser = (Object.prototype.hasOwnProperty.call(userApiToken, "apiToken")); - - // Set custom fonts from the add-on - await setCustomFonts(); - - if (!signedInUser) { - sendRelayEvent("Panel", "viewed-panel", "unauthenticated-user-panel"); - showSignUpPanel(); - } - - if (signedInUser) { - showRelayPanel(1); - } - - - await enableSettingsPanel(); - await enableReportIssuePanel(); - - enableDataOptOut(); - enableInputIconDisabling(); - - document.querySelectorAll(".close-popup-after-click").forEach(el => { - el.addEventListener("click", async (e) => { - e.preventDefault(); - if (e.target.dataset.eventLabel && e.target.dataset.eventAction) { - sendRelayEvent("Panel", e.target.dataset.eventAction, e.target.dataset.eventLabel); + sessionState.loggedIn = await popup.utilities.isUserSignedIn(); + + // Check if user is signed in to show default/sign-in panel + if (sessionState.loggedIn) { + popup.panel.update("masks"); + popup.utilities.unhideNavigationItemsOnceLoggedIn(); + // populateNewsFeed Also sets Notification Bug for Unread News Items + popup.utilities.populateNewsFeed(); + } else { + popup.panel.update("sign-up"); + document.body.classList.remove("is-loading"); } - await browser.tabs.create({ url: el.href }); - window.close(); - }); - }); - - const { relaySiteOrigin } = await browser.storage.local.get("relaySiteOrigin"); - - document.querySelectorAll(".login-link").forEach(loginLink => { - loginLink.href = `${relaySiteOrigin}/accounts/profile?utm_source=fx-relay-addon&utm_medium=popup&utm_content=popup-continue-btn`; - }); + // Set External Event Listerners + await popup.utilities.setExternalLinkEventListeners(); - document.querySelectorAll(".dashboard-link").forEach(dashboardLink => { - dashboardLink.href = `${relaySiteOrigin}/accounts/profile?utm_source=fx-relay-addon&utm_medium=popup&utm_content=manage-relay-addresses`; - }); - - document.querySelectorAll(".get-premium-link").forEach(premiumLink => { - premiumLink.href = `${relaySiteOrigin}/premium?utm_source=fx-relay-addon&utm_medium=popup&utm_content=get-premium-link`; - }); + // Note: There's a chain of functions that run from init, and end with putting focus on the most reasonable element: + // Cases: + // If not logged in: focused on "Sign In" button + // (Both tiers) If no masks made: focused on primary generate mask button + // If free tier: focused on "Create mask" button + // If premium tier: focused in search bar + }, + panel: { + update: (panelId, data) => { + const panels = document.querySelectorAll(".fx-relay-panel"); + panels.forEach((panel) => { + panel.classList.add("is-hidden"); + + if (panel.dataset.panelId === panelId) { + panel.classList.remove("is-hidden"); + popup.panel.init(panelId, data); + } + }); + + sessionState.currentPanel = panelId; + }, + init: (panelId, data) => { + switch (panelId) { + case "custom": + popup.panel.masks.custom.init(); + break; + + case "masks": + popup.panel.masks.init(); + break; + + case "news": + sendRelayEvent("Panel", "click", "opened-news"); + popup.panel.news.init(); + popup.panel.news.utilities.updateNewsItemCountNotification(true); + break; + + case "newsItem": + sendRelayEvent("Panel", "click", "opened-news-item"); + popup.panel.news.item.update(data.newsItemId); + break; + + case "settings": + sendRelayEvent("Panel", "click", "opened-settings"); + popup.panel.settings.init(); + break; + + case "stats": + sendRelayEvent("Panel", "click", "opened-stats"); + popup.panel.stats.init(); + break; + + case "webcompat": + sendRelayEvent("Panel", "click", "opened-report-issue"); + popup.panel.webcompat.init(); + break; + } + }, + masks: { + custom: { + init: async () => { + const customMaskForm = document.querySelector(".fx-relay-panel-custom-mask-form"); + const customMaskDomainInput = customMaskForm.querySelector(".fx-relay-panel-custom-mask-input-name"); + const customMaskDomainLabel = customMaskForm.querySelector(".fx-relay-panel-custom-mask-input-domain"); + const customMaskDomainSubmitButton = customMaskForm.querySelector(".fx-relay-panel-custom-mask-submit button"); + customMaskDomainInput.placeholder = browser.i18n.getMessage("popupCreateCustomFormMaskInputPlaceholder"); + customMaskDomainLabel.textContent = browser.i18n.getMessage("popupCreateCustomFormMaskInputDescription", sessionState.premiumSubdomain); + + customMaskDomainInput.addEventListener("input", popup.panel.masks.custom.validateForm); + customMaskForm.addEventListener("submit", popup.panel.masks.custom.submit); + + const currentPageHostName = await browser.runtime.sendMessage({ + method: "getCurrentPageHostname", + }); + + if (currentPageHostName) { + const parsedDomain = psl.parse(currentPageHostName) + customMaskDomainInput.value = parsedDomain.sld; + customMaskDomainSubmitButton.disabled = false + } + + customMaskDomainInput.focus(); + + }, + submit: async (event) => { + event.preventDefault(); + const customMaskDomainInput = document.getElementById("customMaskName"); + const customMaskBlockPromosCheckbox = document.getElementById("customMaskBlockPromos"); + + if (!customMaskDomainInput.value) { + throw new Error(`No address name set`) + } + + popup.events.generateMask(event, "custom", { + address: customMaskDomainInput.value, + block_list_emails: customMaskBlockPromosCheckbox.checked, + }); + + popup.panel.update("masks"); + + }, + validateForm: async () => { + const customMaskForm = document.querySelector(".fx-relay-panel-custom-mask-form"); + const customMaskDomainInput = customMaskForm.querySelector(".fx-relay-panel-custom-mask-input-name"); + const customMaskDomainSubmitButton = customMaskForm.querySelector(".fx-relay-panel-custom-mask-submit button"); + + // If there's input, make the form submission possible + customMaskDomainSubmitButton.disabled = !(customMaskDomainInput.value) + } + }, + init: async () => { + + const masks = await popup.utilities.getMasks(); + const generateRandomMask = document.querySelector(".js-generate-random-mask"); + const { premium } = await browser.storage.local.get("premium"); + const maskPanel = document.getElementById("masks-panel"); + + if (!premium) { + await popup.panel.masks.utilities.setRemainingMaskCount(); + maskPanel.setAttribute("data-account-level", "free"); + } else { + maskPanel.setAttribute("data-account-level", "premium"); + + // Update language of Generate Random Mask to "Generate random mask" + generateRandomMask.textContent = browser.i18n.getMessage("pageInputIconGenerateRandomMask"); + + // Prompt user to register subdomain + const { premiumSubdomainSet } = await browser.storage.local.get("premiumSubdomainSet"); + const isPremiumSubdomainSet = (premiumSubdomainSet !== "None"); + + // Store this query locally for this session + sessionState.premiumSubdomainSet = isPremiumSubdomainSet; + + // premiumSubdomain is not set : display CTA to prompt user to register subdomain + if (!sessionState.premiumSubdomainSet) { + const registerSubdomainButton = document.querySelector(".fx-relay-regsiter-subdomain-button"); + registerSubdomainButton.classList.remove("is-hidden"); + } else { + + sessionState.premiumSubdomain = premiumSubdomainSet; + const generateCustomMask = document.querySelector(".js-generate-custom-mask"); + + // Show "Generate custom mask" button + generateCustomMask.classList.remove("is-hidden"); + + generateCustomMask.addEventListener("click", (e) => { + e.preventDefault(); + popup.panel.update("custom"); + }, false); + + // Restyle Random Mask button to secondary + generateRandomMask.classList.remove("t-primary"); + generateRandomMask.classList.add("t-secondary"); + } + } + + generateRandomMask.addEventListener("click", (e) => { + popup.events.generateMask(e, "random"); + }, false); + + + // If no masks are created, show onboarding prompt + if (masks.length === 0) { + const noMasksCreatedPanel = document.querySelector(".fx-relay-no-masks-created"); + noMasksCreatedPanel.classList.remove("is-hidden"); + } + + // Build initial list + // Note: If premium, buildMasksList runs `popup.panel.masks.search.init()` after completing + popup.panel.masks.utilities.buildMasksList(); + + + // Remove loading state + document.body.classList.remove("is-loading"); + + }, + search: { + filter: (query)=> { + + const searchInput = document.querySelector(".fx-relay-masks-search-input"); + searchInput.classList.add("is-active"); + + const maskSearchResults = Array.from(document.querySelectorAll(".fx-relay-mask-list li")); + + maskSearchResults.forEach((maskResult) => { + const emailAddress = maskResult.dataset.maskAddress; + const label = maskResult.dataset.maskDescription; + const usedOn = maskResult.dataset.maskUsedOn; + const generated = maskResult.dataset.maskGenerated; + + // Check search input against any mask name, label or used-on/generated for web details + const matchesSearchFilter = + emailAddress.toLowerCase().includes(query.toLowerCase()) || + label.toLowerCase().includes(query.toLowerCase()) || + usedOn.toLowerCase().includes(query.toLowerCase()) || + generated.toLowerCase().includes(query.toLowerCase()); + + if (matchesSearchFilter) { + maskResult.classList.remove("is-hidden"); + } else { + maskResult.classList.add("is-hidden"); + } + + // Set #/# labels inside search bar to show results count + const searchFilterTotal = document.querySelector(".js-filter-masks-total"); + const searchFilterVisible = document.querySelector(".js-filter-masks-visible"); + + searchFilterVisible.textContent = maskSearchResults.filter((maskResult) => !maskResult.classList.contains("is-hidden")).length; + searchFilterTotal.textContent = maskSearchResults.length; + }); + + }, + init: () => { + const searchForm = document.querySelector(".fx-relay-masks-search-form"); + + const searchInput = document.querySelector(".fx-relay-masks-search-input"); + searchInput.placeholder = browser.i18n.getMessage("labelSearch"); + + searchForm.addEventListener("submit", (event) => { + event.preventDefault(); + searchInput.blur(); + }); + + searchInput.addEventListener("input", (event) => { + if (event.target.value.length) { + popup.panel.masks.search.filter(event.target.value); + return; + } + + popup.panel.masks.search.reset() + }); + + searchInput.addEventListener("reset", popup.panel.masks.search.reset); + + const maskSearchResults = Array.from(document.querySelectorAll(".fx-relay-mask-list li")); + const searchFilterTotal = document.querySelector(".js-filter-masks-total"); + const searchFilterVisible = document.querySelector(".js-filter-masks-visible"); + searchFilterVisible.textContent = maskSearchResults.length; + searchFilterTotal.textContent = maskSearchResults.length; + + // Show bar if there's at least one mask created + if (maskSearchResults.length) { + searchForm.classList.add("is-visible"); + searchInput.focus(); + } + }, + reset: () => { + const searchInput = document.querySelector(".fx-relay-masks-search-input"); + searchInput.classList.remove("is-active"); + + const maskSearchResults = Array.from(document.querySelectorAll(".fx-relay-mask-list li")); + const searchFilterTotal = document.querySelector(".js-filter-masks-total"); + const searchFilterVisible = document.querySelector(".js-filter-masks-visible"); + searchFilterVisible.textContent = maskSearchResults.length; + searchFilterTotal.textContent = maskSearchResults.length; + + maskSearchResults.forEach((maskResult) => { + maskResult.classList.remove("is-hidden"); + }); + + } + }, + utilities: { + buildMasksList: async (opts = null) => { + let getMasksOptions = { fetchCustomMasks: false }; + const { premium } = await browser.storage.local.get("premium"); + + if (premium) { + // Check if user may have custom domain masks + const { premiumSubdomainSet } = await browser.storage.local.get( + "premiumSubdomainSet" + ); + + // API Note: If a user has not registered a subdomain yet, its default stored/queried value is "None"; + const isPremiumSubdomainSet = premiumSubdomainSet !== "None"; + getMasksOptions.fetchCustomMasks = isPremiumSubdomainSet; + + // If not set, prompt user to register domain + if (!isPremiumSubdomainSet) { + const registerSubdomainButton = document.querySelector(".fx-relay-regsiter-subdomain-button"); + registerSubdomainButton.classList.remove("is-hidden"); + } + + // Show Generate Button + const generateRandomMask = document.querySelector(".js-generate-random-mask"); + generateRandomMask.classList.remove("is-hidden"); + } + + const masks = await popup.utilities.getMasks(getMasksOptions); + + const maskList = document.querySelector(".fx-relay-mask-list"); + // Reset mask list + maskList.textContent = ""; + + masks.forEach(mask => { + const maskListItem = document.createElement("li"); + + // Attributes used to power search filtering + maskListItem.setAttribute("data-mask-address", mask.full_address); + maskListItem.setAttribute("data-mask-description", mask.description ?? ""); + maskListItem.setAttribute("data-mask-used-on", mask.used_on ?? ""); + maskListItem.setAttribute("data-mask-generated", mask.generated_for ?? ""); + + maskListItem.classList.add("fx-relay-mask-item"); + + const maskListItemNewMaskCreatedLabel = document.createElement("span"); + maskListItemNewMaskCreatedLabel.textContent = browser.i18n.getMessage("labelMaskCreated"); + maskListItemNewMaskCreatedLabel.classList.add("fx-relay-mask-item-new-mask-created"); + maskListItem.appendChild(maskListItemNewMaskCreatedLabel); + + const maskListItemAddressBar = document.createElement("div"); + maskListItemAddressBar.classList.add("fx-relay-mask-item-address-bar"); + + const maskListItemAddressWrapper = document.createElement("div"); + maskListItemAddressWrapper.classList.add("fx-relay-mask-item-address-wrapper"); + + const maskListItemLabel = document.createElement("span"); + maskListItemLabel.classList.add("fx-relay-mask-item-label"); + maskListItemLabel.textContent = mask.description; + + // Append Label if it exists + if (mask.description !== "") { + maskListItemAddressWrapper.appendChild(maskListItemLabel); + } + + const maskListItemAddress = document.createElement("div"); + maskListItemAddress.classList.add("fx-relay-mask-item-address"); + maskListItemAddress.textContent = mask.full_address; + maskListItemAddressWrapper.appendChild(maskListItemAddress); + + // Add Mask Address Bar Contents + maskListItemAddressBar.appendChild(maskListItemAddressWrapper); + + const maskListItemAddressActions = document.createElement("div"); + maskListItemAddressActions.classList.add("fx-relay-mask-item-address-actions"); + + const maskListItemCopyButton = document.createElement("button"); + maskListItemCopyButton.classList.add("fx-relay-mask-item-address-copy"); + maskListItemCopyButton.setAttribute("data-mask-address", mask.full_address); + + const maskListItemCopyButtonSuccessMessage = document.createElement("span"); + maskListItemCopyButtonSuccessMessage.textContent = browser.i18n.getMessage("popupCopyMaskButtonCopied"); + maskListItemCopyButtonSuccessMessage.classList.add("fx-relay-mask-item-address-copy-success"); + maskListItemAddressActions.appendChild(maskListItemCopyButtonSuccessMessage); + + maskListItemCopyButton.addEventListener("click", (e)=> { + e.preventDefault(); + navigator.clipboard.writeText(e.target.dataset.maskAddress); + maskListItemCopyButtonSuccessMessage.classList.add("is-shown"); + setTimeout(() => { + maskListItemCopyButtonSuccessMessage.classList.remove("is-shown") + }, 1000); + }, false); + maskListItemAddressActions.appendChild(maskListItemCopyButton); + + const maskListItemToggleButton = document.createElement("button"); + maskListItemToggleButton.classList.add("fx-relay-mask-item-address-toggle"); + maskListItemToggleButton.addEventListener("click", ()=> { + // TODO: Add Toggle Function + }, false); + maskListItemToggleButton.setAttribute("data-mask-id", mask.id); + maskListItemToggleButton.setAttribute("data-mask-type", mask.mask_type); + maskListItemToggleButton.setAttribute("data-mask-address", mask.full_address); + + // TODO: Add toggle button back + // maskListItemAddressActions.appendChild(maskListItemToggleButton); + + maskListItemAddressBar.appendChild(maskListItemAddressActions); + maskListItem.appendChild(maskListItemAddressBar); + maskList.appendChild(maskListItem); + }); + + // Display "Mask created" temporary label when a new mask is created in the panel + if (opts && opts.newMaskCreated && maskList.firstElementChild) { + maskList.firstElementChild.classList.add("is-new-mask"); + + setTimeout(() => { + maskList.firstElementChild.classList.remove("is-new-mask"); + }, 1000); + } + + // If user has no masks created, focus on random gen button + if (masks.length === 0) { + const generateRandomMask = document.querySelector(".js-generate-random-mask"); + generateRandomMask.focus(); + return; + } + + // If premium, focus on search instead + if (premium) { + popup.panel.masks.search.init(); + } + + }, + getRemainingAliases: async () => { + const masks = await popup.utilities.getMasks(); + const { maxNumAliases } = await browser.storage.local.get("maxNumAliases"); + return { masks, maxNumAliases }; + }, + getRemainingMaskCount: async () => { + const { masks, maxNumAliases } = await popup.panel.masks.utilities.getRemainingAliases(); + const numRemaining = maxNumAliases - masks.length; + return numRemaining; + }, + setRemainingMaskCount: async () => { + const { masks, maxNumAliases } = await popup.panel.masks.utilities.getRemainingAliases(); + const numRemaining = maxNumAliases - masks.length; + const masksAvailable = document.querySelector(".fx-relay-masks-available-count"); + const masksLimitReached = document.querySelector(".fx-relay-masks-limit-upgrade-string"); + const limitReachedToast = document.querySelector(".fx-relay-masks-limit-upgrade"); + + masksAvailable.textContent = browser.i18n.getMessage("popupFreeMasksAvailable", [numRemaining, maxNumAliases]); + masksLimitReached.textContent = browser.i18n.getMessage("popupFreeMasksLimitReached", [maxNumAliases]); + + const generateRandomMask = document.querySelector(".js-generate-random-mask"); + + if (masks.length === 0) { + generateRandomMask.classList.remove("is-hidden"); + return; + } + + if (numRemaining === 0) { + // No masks remaining + limitReachedToast.classList.remove("is-hidden"); + masksAvailable.classList.add("is-hidden"); + + // Hide Generate Button + generateRandomMask.classList.add("is-hidden"); + + // Show Upgrade Button + const getUnlimitedMasksBtn = document.querySelector(".fx-relay-mask-upgrade-button"); + getUnlimitedMasksBtn.classList.remove("is-hidden"); + getUnlimitedMasksBtn.focus(); + + } else { + // Show Masks Count/Generate Button + masksAvailable.classList.remove("is-hidden"); + generateRandomMask.classList.remove("is-hidden"); + generateRandomMask.focus(); + } + } + }, + }, + news: { + init: async () => { + + const newsList = document.querySelector(".fx-relay-news"); + + // If there's any news items, go build them + if ( !newsList.hasChildNodes()) { + sessionState.newsContent.forEach(async (newsItem) => { + + // Build and attach news item + const liFxRelayNewsItem = document.createElement("li"); + liFxRelayNewsItem.classList.add("fx-relay-news-item"); + + const button = document.createElement("button"); + button.classList.add("fx-relay-news-item-button"); + button.setAttribute("data-news-item-id", newsItem.id); + liFxRelayNewsItem.appendChild(button); + + const divTeaserImage = document.createElement("div"); + divTeaserImage.classList.add("fx-relay-news-item-image"); + + const imgTeaserImage = document.createElement("img"); + imgTeaserImage.src = newsItem.teaserImg; + divTeaserImage.appendChild(imgTeaserImage); + button.appendChild(divTeaserImage); + + const divTeaserCopy = document.createElement("div"); + divTeaserCopy.classList.add("fx-relay-news-item-content"); + + const h3TeaserTitle = document.createElement("h3"); + h3TeaserTitle.classList.add("fx-relay-news-item-hero"); + // Pass i18n Args if applicable + const h3TeaserTitleTextContent = newsItem.headlineStringArgs + ? browser.i18n.getMessage( + newsItem.headlineString, + newsItem.headlineStringArgs + ) + : browser.i18n.getMessage(newsItem.headlineString); + h3TeaserTitle.textContent = h3TeaserTitleTextContent; + + const divTeaserBody = document.createElement("div"); + divTeaserBody.classList.add("fx-relay-news-item-body"); + // Pass i18n Args if applicable + const divTeaserBodyTextContent = newsItem.bodyStringArgs + ? browser.i18n.getMessage( + newsItem.bodyString, + newsItem.bodyStringArgs + ) + : browser.i18n.getMessage(newsItem.bodyString); + divTeaserBody.textContent = divTeaserBodyTextContent; + + divTeaserCopy.appendChild(h3TeaserTitle); + divTeaserCopy.appendChild(divTeaserBody); + button.appendChild(divTeaserCopy); + + newsList.appendChild(liFxRelayNewsItem); + + button.addEventListener( + "click", + popup.panel.news.item.show, + false + ); + }); + } + }, + item: { + show: (event) => { + popup.panel.update("newsItem", { + newsItemId: event.target.dataset.newsItemId, + }); + }, + update: (newsItemId) => { + // Get content for news detail view + if (!sessionState.loggedIn) { + return; + } + + const newsItemsContent = sessionState.newsContent.filter((story) => { return story.id == newsItemId }); + const newsItemContent = newsItemsContent[0]; + const newsItemDetail = document.querySelector(".fx-relay-news-story"); + + // Reset news detail item + newsItemDetail.textContent = ""; + + // Populate HTML + const newsItemHeroImage = document.createElement("img"); + newsItemHeroImage.src = newsItemContent.fullImg; + newsItemDetail.appendChild(newsItemHeroImage); + + const newsItemHeroTitle = document.createElement("h3"); + const newsItemHeroTitleTextContent = newsItemContent.headlineStringArgs + ? browser.i18n.getMessage( + newsItemContent.headlineString, + newsItemContent.headlineStringArgs + ) + : browser.i18n.getMessage(newsItemContent.headlineString); + newsItemHeroTitle.textContent = newsItemHeroTitleTextContent; + newsItemDetail.appendChild(newsItemHeroTitle); + + const newsItemHeroBody = document.createElement("div"); + // Pass i18n Args if applicable + const newsItemHeroBodyTextContent = newsItemContent.bodyStringArgs + ? browser.i18n.getMessage( + newsItemContent.bodyString, + newsItemContent.bodyStringArgs + ) + : browser.i18n.getMessage(newsItemContent.bodyString); + newsItemHeroBody.textContent = newsItemHeroBodyTextContent; + newsItemDetail.appendChild(newsItemHeroBody); + + // If the section has a CTA, add it. + if (newsItemContent.fullCta) { + const newsItemHeroCTA = document.createElement("a"); + newsItemHeroCTA.classList.add("fx-relay-news-story-link"); + + // If the URL points towards Relay, choose the correct server + if (newsItemContent.fullCtaRelayURL) { + newsItemHeroCTA.href = `${relaySiteOrigin}${newsItemContent.fullCtaHref}`; + } else { + newsItemHeroCTA.href = `${newsItemContent.fullCtaHref}`; + } + + // Set GA data if applicable + if (newsItemContent.fullCtaEventLabel && newsItemContent.fullCtaEventAction) { + newsItemHeroCTA.setAttribute("data-event-action", newsItemContent.fullCtaEventAction); + newsItemHeroCTA.setAttribute("data-event-label", newsItemContent.fullCtaEventLabel); + } + + newsItemHeroCTA.textContent = browser.i18n.getMessage(newsItemContent.fullCta); + newsItemHeroCTA.addEventListener("click", popup.events.externalClick, false); + newsItemDetail.appendChild(newsItemHeroCTA); + } + }, + }, + utilities: { + initNewsItemCountNotification: async () => { + + const localStorage = await browser.storage.local.get(); + + const unreadNewsItemsCountExists = + Object.prototype.hasOwnProperty.call( + localStorage, + "unreadNewsItemsCount" + ); + + const readNewsItemsCountExists = + Object.prototype.hasOwnProperty.call( + localStorage, + "readNewsItemCount" + ); + + // First-run user: No unread data present + if (!unreadNewsItemsCountExists && !readNewsItemsCountExists) { + await browser.storage.local.set({ + unreadNewsItemsCount: sessionState.newsItemsCount, + readNewsItemCount: 0, + }); + } + + // FIXME: The total news item count may differ than what is displayed to the user + // Example: Three items total but user doesn't have waffle for one news item. + // Regardless - update the unreadNews count to match whatever is in state + await browser.storage.local.set({ + unreadNewsItemsCount: sessionState.newsItemsCount, + }); + + const { readNewsItemCount } = await browser.storage.local.get( + "readNewsItemCount" + ); + + const { unreadNewsItemsCount } = await browser.storage.local.get( + "unreadNewsItemsCount" + ); + + // Set unread count + const newsItemCountNotification = document.querySelector( + ".fx-relay-menu-dashboard-link[data-panel-id='news'] .news-count" + ); + + const unreadCount = unreadNewsItemsCount - readNewsItemCount; + + // Show count is it exists + if (unreadCount > 0) { + newsItemCountNotification.textContent = unreadCount.toString(); + newsItemCountNotification.classList.remove("is-hidden"); + } + + }, + updateNewsItemCountNotification: async (markAllUnread = false) => { + if (markAllUnread) { + await browser.storage.local.set({ + readNewsItemCount: sessionState.newsItemsCount, + }); + + const newsItemCountNotification = document.querySelector( + ".fx-relay-menu-dashboard-link[data-panel-id='news'] .news-count" + ); + + newsItemCountNotification.classList.add("is-hidden"); + + } + } + }, + }, + settings: { + init: () => { + popup.utilities.enableInputIconDisabling(); + + // Function is imported from data-opt-out-toggle.js + enableDataOptOut(); + + const reportWebcompatIssueLink = document.getElementById("popupSettingsReportIssue"); + + if (sessionState.loggedIn) { + reportWebcompatIssueLink.classList.remove("is-hidden"); + reportWebcompatIssueLink.addEventListener("click", (e) => { + e.preventDefault(); + popup.panel.update("webcompat"); + }, false); + } else { + reportWebcompatIssueLink.classList.add("is-hidden"); + } + } + }, + stats: { + init: async () => { + // Check if user is premium (and then check if they have a domain set) + // This is needed in order to query both random and custom masks + const { premium } = await browser.storage.local.get("premium"); + let getMasksOptions = { fetchCustomMasks: false }; + + if (premium) { + // Check if user may have custom domain masks + const { premiumSubdomainSet } = await browser.storage.local.get( + "premiumSubdomainSet" + ); + + // API Note: If a user has not registered a subdomain yet, its default stored/queried value is "None"; + const isPremiumSubdomainSet = premiumSubdomainSet !== "None"; + getMasksOptions.fetchCustomMasks = isPremiumSubdomainSet; + } + + const masks = await popup.utilities.getMasks(getMasksOptions); + + // Get Global Mask Stats data + const totalAliasesUsedVal = masks.length; + let totalEmailsForwardedVal = 0; + let totalEmailsBlockedVal = 0; + + // Loop through all masks to calculate totals + masks.forEach((mask) => { + totalEmailsForwardedVal += mask.num_forwarded; + totalEmailsBlockedVal += mask.num_blocked; + }); + + // Set global stats data + const globalStatSet = document.querySelector(".dashboard-stats-list.global-stats"); + const globalAliasesUsedValEl = globalStatSet.querySelector(".aliases-used"); + const globalEmailsBlockedValEl = globalStatSet.querySelector(".emails-blocked"); + const globalEmailsForwardedValEl = globalStatSet.querySelector(".emails-forwarded"); + + globalAliasesUsedValEl.textContent = totalAliasesUsedVal; + globalEmailsBlockedValEl.textContent = totalEmailsForwardedVal; + globalEmailsForwardedValEl.textContent = totalEmailsBlockedVal; + + // Get current page + const currentPageHostName = await browser.runtime.sendMessage({ + method: "getCurrentPageHostname", + }); + + // Check if any data applies to the current site + if ( popup.utilities.checkIfAnyMasksWereGeneratedOnCurrentWebsite(masks,currentPageHostName) ) { + + // Some masks are used on the current site. Time to calculate! + const filteredMasks = masks.filter( + (mask) => + mask.generated_for === currentPageHostName || + popup.utilities.hasMaskBeenUsedOnCurrentSite( + mask, + currentPageHostName + ) + ); + + let currentWebsiteForwardedVal = 0; + let currentWebsiteBlockedVal = 0; + + // Calculate forward/blocked counts + filteredMasks.forEach((mask) => { + currentWebsiteForwardedVal += mask.num_forwarded; + currentWebsiteBlockedVal += mask.num_blocked; + }); + + // Set current website usage data + const currentWebsiteStateSet = document.querySelector(".dashboard-stats-list.current-website-stats"); + + const currentWebsiteAliasesUsedValEl = currentWebsiteStateSet.querySelector(".aliases-used"); + currentWebsiteAliasesUsedValEl.textContent = filteredMasks.length; + + const currentWebsiteEmailsForwardedValEl = currentWebsiteStateSet.querySelector(".emails-forwarded"); + currentWebsiteEmailsForwardedValEl.textContent = currentWebsiteForwardedVal; + + const currentWebsiteEmailsBlockedValEl = currentWebsiteStateSet.querySelector(".emails-blocked"); + currentWebsiteEmailsBlockedValEl.textContent = currentWebsiteBlockedVal; + + // If there's usage data for current website stats, show it + const currentWebsiteEmailsBlocked = currentWebsiteStateSet.querySelector(".dashboard-info-emails-blocked"); + const currentWebsiteEmailsForwarded = currentWebsiteStateSet.querySelector(".dashboard-info-emails-forwarded"); + currentWebsiteEmailsBlocked.classList.remove("is-hidden"); + currentWebsiteEmailsForwarded.classList.remove("is-hidden"); + + } + }, + }, + webcompat: { + init: () => { + popup.panel.webcompat.setURLwithIssue(); + popup.panel.webcompat.setRequiredCheckboxListeners(); + popup.panel.webcompat.showReportInputOtherTextField(); + + const reportForm = document.querySelector(".report-issue-content"); + reportForm.addEventListener("submit", async (event) => { + await popup.panel.webcompat.handleReportIssueFormSubmission(event); + }); + + const reportContinueButton = + document.querySelector(".report-continue"); + reportContinueButton.addEventListener( + "click", + popup.events.backClick, + false + ); + }, + toggleRequiredCheckboxListeners: (toggleOverride = null) => { + const checkboxes = document.querySelectorAll('.report-section ul li input'); + + checkboxes.forEach(checkbox => { + // Override arg must be present to override + if (toggleOverride === null) { + checkbox.required = !checkbox.required; + } else { + checkbox.required = toggleOverride; + } + }); + }, + setRequiredCheckboxListeners: () => { + const reportIssueSubmitBtn = document.querySelector(".report-issue-submit-btn"); + const inputFieldUrl = document.querySelector('input[name="issue_on_domain"]'); + const checkboxes = document.querySelectorAll('.report-section ul li input'); + const otherTextField = document.querySelector('input[name="other_issue"]'); + const isChecked = (element) => element.checked; + + checkboxes.forEach(checkbox => { + checkbox.required = true; + checkbox.addEventListener("change", ()=> { + // If the user has selected at least one checkbox, and has filled out the website URL input, make the form submittable + if ([...checkboxes].some(isChecked) && inputFieldUrl.value && popup.utilities.isSortaAURL(inputFieldUrl.value) && checkbox.name !== "issue-case-other") { + reportIssueSubmitBtn.disabled = false; + popup.panel.webcompat.toggleRequiredCheckboxListeners(false); + } else { + reportIssueSubmitBtn.disabled = true; + popup.panel.webcompat.toggleRequiredCheckboxListeners(true); + } + + // Custom logic if the user clicks the "other" box + if (checkbox.name === "issue-case-other" && checkbox.checked && otherTextField.value && popup.utilities.isSortaAURL(otherTextField.value)) { + reportIssueSubmitBtn.disabled = false; + popup.panel.webcompat.toggleRequiredCheckboxListeners(false); + } + }) + }); + + }, + setURLwithIssue: async () => { + // Add Site URL placeholder + const currentPage = (await popup.utilities.getCurrentPage()).url; + const reportIssueSubmitBtn = document.querySelector(".report-issue-submit-btn"); + const inputFieldUrl = document.querySelector('input[name="issue_on_domain"]'); + const checkboxes = document.querySelectorAll('.report-section ul li input'); + const isChecked = (element) => element.checked; + + reportIssueSubmitBtn.disabled = true; + + // Allow for custom URL inputs + inputFieldUrl.addEventListener("input", () => { + reportIssueSubmitBtn.disabled = true; + // Ensure that the custom input looks like a URL without https:// or http:// (e.g. test.com, www.test.com) + // AND at least one checkbox is checked + if (popup.utilities.isSortaAURL(inputFieldUrl.value) && [...checkboxes].some(isChecked)) { + reportIssueSubmitBtn.disabled = false; + } + }); + + // Check that the host site has a valid URL + if (currentPage) { + const url = new URL(currentPage); + // returns a http:// or https:// value + inputFieldUrl.value = url.origin; + } + }, + showReportInputOtherTextField: () => { + const otherCheckbox = document.querySelector('input[name="issue-case-other"]'); + const otherTextField = document.querySelector('input[name="other_issue"]'); + const reportIssueSubmitBtn = document.querySelector(".report-issue-submit-btn"); + + otherCheckbox.addEventListener("click", () => { + otherTextField.classList.toggle("is-hidden"); + + if (!otherTextField.classList.contains("is-hidden")) { + // If the user has checked "Other", they must add text to the "Other" text input before submitting + reportIssueSubmitBtn.disabled = true; + otherTextField.required = true; + } else { + otherTextField.required = false; + } + }); + + + + // Add placeholder to report input on 'Other' selection + const inputFieldOtherDetails = document.querySelector( + 'input[name="other_issue"]' + ); + + // Allow for custom URL inputs + inputFieldOtherDetails.addEventListener("input", () => { + reportIssueSubmitBtn.disabled = true; + // Ensure that the custom input looks like a URL without https:// or http:// (e.g. test.com, www.test.com) + // AND at least one checkbox is checked + if (popup.utilities.isSortaAURL(inputFieldOtherDetails.value)) { + reportIssueSubmitBtn.disabled = false; + } + }); + + inputFieldOtherDetails.placeholder = browser.i18n.getMessage( + "popupReportIssueCaseOtherDetails" + ); + }, + handleReportIssueFormSubmission: async (event) => { + event.preventDefault(); + const data = new FormData(event.target); + const reportData = Object.fromEntries(data.entries()); + reportData.user_agent = await getBrowser(); + + Object.keys(reportData).forEach(function (value) { + // Switch "on" to true + if (reportData[value] === "on") { + reportData[value] = true; + } + // Remove from report if empty string + if (reportData[value] === "") { + delete reportData[value]; + } + }); + + // Clean URL data to add "http://" before it if the custom input doesn't contain a HTTP protocol + if ( + !( + reportData.issue_on_domain.startsWith("http://") || + reportData.issue_on_domain.startsWith("https://") + ) + ) { + reportData.issue_on_domain = "http://" + reportData.issue_on_domain; + } + + await browser.runtime.sendMessage({ + method: "postReportWebcompatIssue", + description: reportData, + }); + }, + }, + }, + utilities: { + checkIfAnyMasksWereGeneratedOnCurrentWebsite: (masks, domain) => { + return masks.some((mask) => { + return domain === mask.generated_for; + }); + }, + clearBrowserActionBadge: async () => { + const { browserActionBadgesClicked } = await browser.storage.local.get( + "browserActionBadgesClicked" + ); + + // Dismiss the browserActionBadge only when it exists + if (browserActionBadgesClicked === false) { + browser.storage.local.set({ browserActionBadgesClicked: true }); + browser.browserAction.setBadgeBackgroundColor({ color: null }); + browser.browserAction.setBadgeText({ text: "" }); + } + }, + enableInputIconDisabling: async () => { + const inputIconVisibilityToggle = document.querySelector( + ".toggle-icon-in-page-visibility" + ); + + const stylePrefToggle = (inputsEnabled) => { + if (inputsEnabled === "show-input-icons") { + inputIconVisibilityToggle.dataset.iconVisibilityOption = + "disable-input-icon"; + inputIconVisibilityToggle.classList.remove("input-icons-disabled"); + return; + } + inputIconVisibilityToggle.dataset.iconVisibilityOption = + "enable-input-icon"; + inputIconVisibilityToggle.classList.add("input-icons-disabled"); + }; + + const iconsAreEnabled = await areInputIconsEnabled(); + const userIconChoice = iconsAreEnabled + ? "show-input-icons" + : "hide-input-icons"; + stylePrefToggle(userIconChoice); + + inputIconVisibilityToggle.addEventListener("click", async () => { + const userIconPreference = + inputIconVisibilityToggle.dataset.iconVisibilityOption === + "disable-input-icon" + ? "hide-input-icons" + : "show-input-icons"; + await browser.runtime.sendMessage({ + method: "updateInputIconPref", + iconPref: userIconPreference, + }); + sendRelayEvent("Panel", "click", userIconPreference); + return stylePrefToggle(userIconPreference); + }); + }, + hasMaskBeenUsedOnCurrentSite: (mask, domain) => { + const domainList = mask.used_on; - document.querySelectorAll(".register-domain-cta").forEach(registerDomainLink => { - registerDomainLink.href = `${relaySiteOrigin}/accounts/profile?utm_source=fx-relay-addon&utm_medium=popup&utm_content=register-email-domain#mpp-choose-subdomain`; - }); + // Short circuit out if there's no used_on entry + if ([undefined, null, ""].includes(domainList)) { + return false; + } - // Add backlink to pricing section from promo CTAs - const promoCTAEl = document.querySelectorAll(".js-promo-link"); - promoCTAEl.forEach(i => { - i.href = `${relaySiteOrigin}/premium#pricing`; - }) + // Domain already exists in used_on field. Just return the list! + if (domainList.split(",").includes(domain)) { + return true; + } -} + // No match found! + return false; + }, + isSortaAURL: (str) => { + return str.includes(".") && !str.endsWith(".") && !str.startsWith("."); + }, + isUserSignedIn: async () => { + const userApiToken = await browser.storage.local.get("apiToken"); + const signedInUser = Object.prototype.hasOwnProperty.call( + userApiToken, + "apiToken" + ); + return signedInUser; + }, + getCachedServerStoragePref: async () => { + const serverStoragePref = await browser.storage.local.get( + "server_storage" + ); + const serverStoragePrefInLocalStorage = + Object.prototype.hasOwnProperty.call( + serverStoragePref, + "server_storage" + ); + + if (!serverStoragePrefInLocalStorage) { + // There is no reference to the users storage preference saved. Fetch it from the server. + return await browser.runtime.sendMessage({ + method: "getServerStoragePref", + }); + } else { + // If the stored pref exists, return value + return serverStoragePref.server_storage; + } + }, + getCurrentPage: async () => { + const [currentTab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + return currentTab; + }, + getMasks: async (options = { fetchCustomMasks: false }) => { + const serverStoragePref = + await popup.utilities.getCachedServerStoragePref(); + + if (serverStoragePref) { + try { + return await browser.runtime.sendMessage({ + method: "getAliasesFromServer", + options, + }); + } catch (error) { + console.warn(`getAliasesFromServer Error: ${error}`); + + // API Error — Fallback to local storage + const { relayAddresses } = await browser.storage.local.get( + "relayAddresses" + ); + + return relayAddresses; + } + } + + // User is not syncing with the server. Use local storage. + const { relayAddresses } = await browser.storage.local.get("relayAddresses"); + return relayAddresses; + }, + populateNewsFeed: async ()=> { + // audience can be premium, free, phones, all + // Optional data: waffle, fullCta* + const savings = "40%"; // For "Save 40%!" in the Bundle promo body + + const isBundleAvailableInCountry = (await browser.storage.local.get("bundlePlans")).bundlePlans.BUNDLE_PLANS.available_in_country; + const isPhoneAvailableInCountry = (await browser.storage.local.get("phonePlans")).phonePlans.PHONE_PLANS.available_in_country; + const hasPhone = (await browser.storage.local.get("has_phone")).has_phone; + const hasVpn = (await browser.storage.local.get("has_vpn")).has_vpn; + + // Conditions for phone masking announcement to be shown: if the user is in US/CAN, phone flag is on, and user has not purchased phone plan yet + const isPhoneMaskingAvailable = isPhoneAvailableInCountry && !hasPhone; + + // Conditions for bundle announcement to be shown: if the user is in US/CAN, bundle flag is on, and user has not purchased bundle plan yet + const isBundleAvailable = isBundleAvailableInCountry && !hasVpn; + + // Conditions for firefox integration to be shown: if the waffle flag "firefox_integration" is set as true + const isFirefoxIntegrationAvailable = await checkWaffleFlag("firefox_integration"); + + // FIXME: The order is not being set correctly + if (isFirefoxIntegrationAvailable) { + sessionState.newsContent.push({ + id: "firefox-integration", + dateAdded: "20230314", // YYYYMMDD + waffle: "firefox_integration", + locale: "us", + audience: "premium", + headlineString: "popupPasswordManagerRelayHeadline", + bodyString: "popupPasswordManagerRelayBody", + teaserImg: + "/images/panel-images/announcements/panel-announcement-password-manager-relay-square-illustration.svg", + fullImg: + "/images/panel-images/announcements/panel-announcement-password-manager-relay-illustration.svg", + }); + } + + // Add Phone Masking News Item + if (isPhoneMaskingAvailable) { + sessionState.newsContent.push({ + id: "phones", + dateAdded: "20221006", // YYYYMMDD + headlineString: "popupPhoneMaskingPromoHeadline", + bodyString: "popupPhoneMaskingPromoBody", + teaserImg: + "/images/panel-images/announcements/premium-announcement-phone-masking.svg", + fullImg: + "/images/panel-images/announcements/premium-announcement-phone-masking-hero.svg", + fullCta: "popupPhoneMaskingPromoCTA", + fullCtaRelayURL: true, + fullCtaHref: + "/premium/?utm_source=fx-relay-addon&utm_medium=popup&utm_content=panel-news-phone-masking-cta#pricing", + fullCtaEventLabel: "panel-news-phone-masking-cta", + fullCtaEventAction: "click", + }); + } + + // Add Bundle Pricing News Item + if (isBundleAvailable) { + const getBundlePlans = (await browser.storage.local.get("bundlePlans")).bundlePlans.BUNDLE_PLANS; + const getBundlePrice = getBundlePlans.plan_country_lang_mapping[getBundlePlans.country_code].en.yearly.price; + const getBundleCurrency = getBundlePlans.plan_country_lang_mapping[getBundlePlans.country_code].en.yearly.currency + const userLocale = navigator.language; + const formattedBundlePrice = new Intl.NumberFormat(userLocale, { + style: "currency", + currency: getBundleCurrency, + }).format(getBundlePrice); + + sessionState.newsContent.push({ + id: "mozilla-vpn-bundle", + dateAdded: "20221025", // YYYYMMDD + headlineString: "popupBundlePromoHeadline_2", + headlineStringArgs: savings, + bodyString: "popupBundlePromoBody_3", + bodyStringArgs: formattedBundlePrice, + teaserImg: + "/images/panel-images/announcements/panel-bundle-announcement-square.svg", + fullImg: + "/images/panel-images/announcements/panel-bundle-announcement.svg", + fullCta: "popupPhoneMaskingPromoCTA", + fullCtaRelayURL: true, + fullCtaHref: + "/premium/?utm_source=fx-relay-addon&utm_medium=popup&utm_content=panel-news-bundle-cta#pricing", + fullCtaEventLabel: "panel-news-bundle-cta", + fullCtaEventAction: "click", + },) + } + + // Remove news nav link if there's no news items to display to user + if (sessionState.newsContent.length === 0 ) { + document.querySelector(".fx-relay-menu-dashboard-link[data-panel-id='news']").remove(); + return; + } + + // Sort news items by dateAdded field (Newest at the top) + sessionState.newsContent.sort((a, b) => (a.dateAdded < b.dateAdded ? 1 : -1)); + + // Update news item count + sessionState.newsItemsCount = sessionState.newsContent.length; + + // Set unread notification count + await popup.panel.news.utilities.initNewsItemCountNotification(); + }, + setExternalLinkEventListeners: async () => { + const externalLinks = document.querySelectorAll(".js-external-link"); + + externalLinks.forEach((link) => { + // Because we dynamically set the Relay origin URL (local/dev/stage/prod), + // we have to catch Relay-specific links and prepend the correct Relay website URL + if (link.dataset.relayInternal === "true") { + // TODO: Remove "/" from here. It'll be error prone + link.href = `${relaySiteOrigin}/${link.dataset.href}`; + } else { + link.href = `${link.dataset.href}`; + } + + link.addEventListener("click", popup.events.externalClick, false); + }); + }, + unhideNavigationItemsOnceLoggedIn: () => { + document + .querySelectorAll(".fx-relay-menu-dashboard-link.is-hidden") + .forEach((link) => { + link.classList.remove("is-hidden"); + }); + }, + }, + }; -document.addEventListener("DOMContentLoaded", popup); + popup.init(); +})(); diff --git a/src/js/shared/i18n.js b/src/js/shared/i18n.js index 89e7bb9f7..1d7e4f5d5 100644 --- a/src/js/shared/i18n.js +++ b/src/js/shared/i18n.js @@ -7,6 +7,11 @@ document.addEventListener("DOMContentLoaded", async () => { el.value = browser.i18n.getMessage(el.dataset.i18nMessageId); } }); + + const i18nContentAltTags = document.querySelectorAll(".i18n-alt-tag"); + i18nContentAltTags.forEach(el => { + el.alt = browser.i18n.getMessage(el.dataset.i18nMessageId); + }); const i18nContentAttributes = document.querySelectorAll(".i18n-attribute"); i18nContentAttributes.forEach(el => { diff --git a/src/js/shared/metrics.js b/src/js/shared/metrics.js index 7e1031b94..cba2b4ab0 100644 --- a/src/js/shared/metrics.js +++ b/src/js/shared/metrics.js @@ -1,10 +1,9 @@ -"use strict"; - +/* global getBrowser */ /* exported sendRelayEvent */ // eslint-disable-next-line no-redeclare async function sendRelayEvent(eventCategory, eventAction, eventLabel) { - // "dimension5" is a Google Analytics-specific variable to track a custom dimension. + // "dimension5" is a Google Analytics-specific variable to track a custom dimension. // This dimension is used to determine which browser vendor the add-on is using: Firefox or Chrome // "dimension7" is a Google Analytics-specific variable to track a custom dimension, // used to determine where the ping is coming from: website, add-on or app @@ -19,15 +18,3 @@ async function sendRelayEvent(eventCategory, eventAction, eventLabel) { }, }); } - -async function getBrowser() { - if (typeof browser.runtime.getBrowserInfo === "function") { - /** @type {{ name: string, vendor: string, version: string, buildID: string }} */ - const browserInfo = await browser.runtime.getBrowserInfo(); - return browserInfo.name; - } - if (navigator.userAgent.toLowerCase().indexOf("firefox") !== -1) { - return "Firefox"; - } - return "Chrome"; -} diff --git a/src/js/shared/utils.js b/src/js/shared/utils.js index 504359e59..d770f57c5 100644 --- a/src/js/shared/utils.js +++ b/src/js/shared/utils.js @@ -1,4 +1,4 @@ -/* exported areInputIconsEnabled setCustomFonts preventDefaultBehavior */ +/* exported areInputIconsEnabled setCustomFonts preventDefaultBehavior checkWaffleFlag getBrowser */ // eslint-disable-next-line no-redeclare async function areInputIconsEnabled() { @@ -42,3 +42,27 @@ function preventDefaultBehavior(clickEvt) { clickEvt.preventDefault(); return; } + +// eslint-disable-next-line no-unused-vars +async function checkWaffleFlag(flag) { + const waffleFlagArray = (await browser.storage.local.get("waffleFlags")).waffleFlags.WAFFLE_FLAGS; + for (let i of waffleFlagArray) { + if (i[0] === flag && i[1] === true) { + return true; + } + } + return false; +} + +// eslint-disable-next-line no-unused-vars +async function getBrowser() { + if (typeof browser.runtime.getBrowserInfo === "function") { + /** @type {{ name: string, vendor: string, version: string, buildID: string }} */ + const browserInfo = await browser.runtime.getBrowserInfo(); + return browserInfo.name; + } + if (navigator.userAgent.toLowerCase().indexOf("firefox") !== -1) { + return "Firefox"; + } + return "Chrome"; +} diff --git a/src/manifest.json b/src/manifest.json index 7f49f8539..735ea0b14 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Firefox Relay", - "version": "2.6.1", + "version": "2.7.0", "description": "__MSG_extensionDescription_mask__", diff --git a/src/popup.html b/src/popup.html index 8913a85dc..ffffa0e11 100644 --- a/src/popup.html +++ b/src/popup.html @@ -3,10 +3,10 @@ Firefox Relay - - + + @@ -14,225 +14,375 @@ - - - - - - - - - -
- -

-
- -
- - - -
- -

+ +
+
+

+ + Firefox Relay +

+
-
- -
- - -
-
- - -
    -
  • - - - -
  • -
  • - - - -
  • -
  • - - - -
  • -
  • - - - -
  • -
  • - - -
  • -
+
+ + +
+
+
+
+
- - - - - - - - - -