diff --git a/docs/METRICS.md b/docs/METRICS.md index 1f7a92cb95..aebfc6c29d 100644 --- a/docs/METRICS.md +++ b/docs/METRICS.md @@ -316,6 +316,8 @@ These are events that an add-on user can encounter on a shot they own 37. [x] Remove shot from favorites `web-unset-favorite/navbar` 38. [x] Signin to Firefox Accounts `web/fxa-signin` 39. [x] Signin to Firefox Accounts from banner `web/fxa-signin-ad-banner` +40. [x] Signin to Firefox Accounts from onboarding promo `web/fxa-signin-onboarding-promo` +41. [x] Click on dismiss of Firefox Accounts onboarding promo `web/onboarding-promo-closed` #### Shot Index (My Shots) diff --git a/locales/en-US/server.ftl b/locales/en-US/server.ftl index a263980c0f..4e3afac967 100644 --- a/locales/en-US/server.ftl +++ b/locales/en-US/server.ftl @@ -18,6 +18,17 @@ screenshotsLogo = bannerSignIn = Sign in or sign up to access your shots across devices and save your favorites forever. bannerUpsell = {gScreenshotsDescription} Get Firefox now +# Text used in Firefox Account onboarding promo shown below +# Sign in button in header +onboardingPromoTitle = What’s new with Firefox Screenshots? +onboardingPromoMessage = Now, sign in to Screenshots with a Firefox Account and do more: +onboardingPromoMessageListItem1 = Access your library on all of your devices +onboardingPromoMessageListItem2 = Store your favorite shots forever +onboardingPromoDismissButton = Dismiss + .title = Dismiss +onboardingPromoSigninButton = Sign In + .title = Sign In + ## Footer # Note: link text for a link to mozilla.org diff --git a/server/src/ad-banner.js b/server/src/ad-banner.js index 2647e8ef38..3c37575ea2 100644 --- a/server/src/ad-banner.js +++ b/server/src/ad-banner.js @@ -2,6 +2,7 @@ const React = require("react"); const PropTypes = require("prop-types"); const { Localized } = require("fluent-react/compat"); const sendEvent = require("./browser-send-event.js"); +const { PromotionStrategy } = require("./promotion-strategy.js"); exports.AdBanner = class AdBanner extends React.Component { constructor(props) { @@ -18,8 +19,9 @@ exports.AdBanner = class AdBanner extends React.Component { render() { let bannerContent = null; + const promoStrategy = new PromotionStrategy(); - if (this.props.shouldGetFirefox && !this.props.isOwner) { + if (promoStrategy.shouldShowFirefoxBanner(this.props.shouldGetFirefox, this.props.isOwner)) { const upsellLink = Get Firefox now; @@ -28,7 +30,7 @@ exports.AdBanner = class AdBanner extends React.Component { Screenshots made simple. Take, save and share screenshots without leaving Firefox. {upsellLink}

; - } else if (!this.props.hasFxa) { + } else if (promoStrategy.shouldShowFxaBanner(this.props.hasFxa)) { const signInLink = ; diff --git a/server/src/fxa-onboarding-dialog.js b/server/src/fxa-onboarding-dialog.js new file mode 100644 index 0000000000..3dd63932d9 --- /dev/null +++ b/server/src/fxa-onboarding-dialog.js @@ -0,0 +1,66 @@ +const React = require("react"); +const PropTypes = require("prop-types"); +const { Localized } = require("fluent-react/compat"); +const sendEvent = require("./browser-send-event.js"); + +exports.FxaOnboardingDialog = class FxaOnboardingDialog extends React.Component { + constructor(props) { + super(props); + } + + render() { + if (this.props.display) { + return
+
+
+ + + What’s new with Firefox Screenshots? + +
+
+ +

+ Now, sign in to Screenshots with a Firefox Account and + do more: +

+
+ +
+
+ + Dismiss + + + + +
+
; + } + return null; + } + + onCloseDialog(event) { + this.props.hideDialog(); + sendEvent("onboarding-promo-closed"); + } + + onConfirm(event) { + this.props.hideDialog(); + sendEvent("fxa-signin-onboarding-promo"); + location.href = this.props.logInURI; + } +}; + +exports.FxaOnboardingDialog.propTypes = { + logInURI: PropTypes.string, + display: PropTypes.bool, + hideDialog: PropTypes.func, +}; diff --git a/server/src/header.js b/server/src/header.js index a6ad3d30a3..f3c65ff22f 100644 --- a/server/src/header.js +++ b/server/src/header.js @@ -10,8 +10,14 @@ exports.Header = function Header(props) { : null; + const banner = !props.hasFxaOnboardingDialog ? + : null; + return [ - , + banner,
{logo} {props.children} @@ -25,4 +31,5 @@ exports.Header.propTypes = { isOwner: PropTypes.bool, hasFxa: PropTypes.bool, shouldGetFirefox: PropTypes.bool, + hasFxaOnboardingDialog: PropTypes.bool, }; diff --git a/server/src/pages/homepage/controller.js b/server/src/pages/homepage/controller.js index 8c5f9f4310..2193dd939e 100644 --- a/server/src/pages/homepage/controller.js +++ b/server/src/pages/homepage/controller.js @@ -1,5 +1,6 @@ /* globals Mozilla */ const page = require("./page").page; +const { PromotionStrategy } = require("../../promotion-strategy.js"); let model; @@ -15,6 +16,7 @@ exports.launch = function(m) { }); } } + model.hasFxaOnboardingDialog = new PromotionStrategy().shouldShowOnboardingDialog(); render(); }; diff --git a/server/src/pages/homepage/homepage-header.js b/server/src/pages/homepage/homepage-header.js index 8dbe8ce1a0..212805935c 100644 --- a/server/src/pages/homepage/homepage-header.js +++ b/server/src/pages/homepage/homepage-header.js @@ -17,7 +17,8 @@ exports.HomePageHeader = class HomePageHeader extends React.Component { renderFxASignIn() { return ( + staticLink={this.props.staticLink} + hasFxaOnboardingDialog={this.props.hasFxaOnboardingDialog} /> ); } @@ -34,7 +35,8 @@ exports.HomePageHeader = class HomePageHeader extends React.Component { const signin = this.renderFxASignIn(); return ( -
+
{ myShots } { signin } @@ -48,4 +50,5 @@ exports.HomePageHeader.propTypes = { hasFxa: PropTypes.bool, isOwner: PropTypes.bool, staticLink: PropTypes.func, + hasFxaOnboardingDialog: PropTypes.bool, }; diff --git a/server/src/pages/homepage/view.js b/server/src/pages/homepage/view.js index 64d4e8b2f4..98e166f946 100644 --- a/server/src/pages/homepage/view.js +++ b/server/src/pages/homepage/view.js @@ -104,6 +104,7 @@ class Body extends React.Component { isOwner={this.props.authenticated} hasFxa={this.props.hasFxa} staticLink={this.props.staticLink} + hasFxaOnboardingDialog={this.props.hasFxaOnboardingDialog} />
diff --git a/server/src/pages/shot/controller.js b/server/src/pages/shot/controller.js index cda1897183..f45d5f37a1 100644 --- a/server/src/pages/shot/controller.js +++ b/server/src/pages/shot/controller.js @@ -5,37 +5,11 @@ const page = require("./page").page; const { AbstractShot } = require("../../../shared/shot"); const { createThumbnailUrl } = require("../../../shared/thumbnailGenerator"); const { shotGaFieldForValue } = require("../../ab-tests.js"); +const { PromotionStrategy } = require("../../promotion-strategy.js"); // This represents the model we are rendering: let model; -function shouldHighlightEditIcon(model) { - if (!model.isOwner) { - return false; - } - const hasSeen = localStorage.hasSeenEditButton; - if (!hasSeen && model.enableAnnotations) { - localStorage.hasSeenEditButton = "1"; - } - return !hasSeen; -} - -function shouldShowPromo(model) { - if (!model.isOwner || !model.enableAnnotations) { - return false; - } - let show = false; - const count = localStorage.hasSeenPromoDialog; - if (!count) { - localStorage.hasSeenPromoDialog = 1; - show = true; - } else if (count < 3) { - localStorage.hasSeenPromoDialog = parseInt(count, 10) + 1; - show = true; - } - return show; -} - function updateModel(authData) { Object.assign(model, authData); model.isExtInstalled = true; @@ -75,8 +49,14 @@ exports.launch = function(data) { } } } - model.highlightEditButton = shouldHighlightEditIcon(model); - model.promoDialog = shouldShowPromo(model); + + const promoStrategy = new PromotionStrategy(); + + model.hasFxaOnboardingDialog = promoStrategy.shouldShowOnboardingDialog(model.isOwner, true); + model.highlightEditButton = + promoStrategy.shouldHighlightEditIcon(model.isOwner, model.enableAnnotations); + model.promoDialog = + promoStrategy.shouldShowEditToolPromotion(model.isOwner, model.enableAnnotations, model.hasFxaOnboardingDialog); if (firstSet) { refreshHash(); diff --git a/server/src/pages/shot/shotpage-header.js b/server/src/pages/shot/shotpage-header.js index f3ab290ce9..d7e093920a 100644 --- a/server/src/pages/shot/shotpage-header.js +++ b/server/src/pages/shot/shotpage-header.js @@ -90,10 +90,9 @@ exports.ShotPageHeader = class ShotPageHeader extends React.Component { renderFxASignIn() { if (this.props.isOwner) { return ( -
- -
+ ); } return null; @@ -114,7 +113,7 @@ exports.ShotPageHeader = class ShotPageHeader extends React.Component { return (
+ hasFxa={this.props.hasFxa} hasFxaOnboardingDialog={this.props.hasFxaOnboardingDialog}> { myShotsText } { shotInfo } { shotActions } @@ -134,6 +133,7 @@ exports.ShotPageHeader.propTypes = { shouldGetFirefox: PropTypes.bool, shotActions: PropTypes.array, staticLink: PropTypes.func, + hasFxaOnboardingDialog: PropTypes.bool, }; class EditableTitle extends React.Component { diff --git a/server/src/pages/shot/view.js b/server/src/pages/shot/view.js index 9ddde4a3ad..28f34c73a5 100644 --- a/server/src/pages/shot/view.js +++ b/server/src/pages/shot/view.js @@ -446,7 +446,8 @@ class Body extends React.Component {
+ staticLink={this.props.staticLink} shotActions={shotActions} + hasFxaOnboardingDialog={this.props.hasFxaOnboardingDialog}> { !this.props.isOwner ? downloadButton : null }
diff --git a/server/src/pages/shotindex/controller.js b/server/src/pages/shotindex/controller.js index a20cc7474e..81b91bf1fd 100644 --- a/server/src/pages/shotindex/controller.js +++ b/server/src/pages/shotindex/controller.js @@ -2,6 +2,7 @@ const sendEvent = require("../../browser-send-event.js"); const page = require("./page").page; const { AbstractShot } = require("../../../shared/shot"); const queryString = require("query-string"); +const { PromotionStrategy } = require("../../promotion-strategy.js"); const FIVE_SECONDS = 5 * 1000; @@ -65,6 +66,11 @@ exports.launch = function(m) { // The actual shot data hasn't been loaded yet, so we'll immediately request it: refreshModel(); } + + if (!firstRun) { + model.hasFxaOnboardingDialog = new PromotionStrategy().shouldShowOnboardingDialog(); + } + firstRun = false; render(); }; diff --git a/server/src/pages/shotindex/myshots-header.js b/server/src/pages/shotindex/myshots-header.js index a40b9e6b31..9c3b9109d8 100644 --- a/server/src/pages/shotindex/myshots-header.js +++ b/server/src/pages/shotindex/myshots-header.js @@ -7,10 +7,12 @@ exports.MyShotsHeader = function MyShotsHeader(props) { const signin = props.enableUserSettings && props.authenticated ? : null; + staticLink={props.staticLink} + hasFxaOnboardingDialog={props.hasFxaOnboardingDialog} /> : null; return ( -
+
{ signin }
@@ -23,4 +25,5 @@ exports.MyShotsHeader.propTypes = { authenticated: PropTypes.bool, enableUserSettings: PropTypes.bool, staticLink: PropTypes.func, + hasFxaOnboardingDialog: PropTypes.bool, }; diff --git a/server/src/pages/shotindex/view.js b/server/src/pages/shotindex/view.js index 6190b85399..a9e7291711 100644 --- a/server/src/pages/shotindex/view.js +++ b/server/src/pages/shotindex/view.js @@ -42,7 +42,8 @@ class Body extends React.Component {
+ enableUserSettings={this.props.enableUserSettings} staticLink={this.props.staticLink} + hasFxaOnboardingDialog={this.props.hasFxaOnboardingDialog} /> { this.props.disableSearch ? null : this.renderSearchForm() }
{ this.renderShots() } diff --git a/server/src/promotion-strategy.js b/server/src/promotion-strategy.js new file mode 100644 index 0000000000..1189e8d2d9 --- /dev/null +++ b/server/src/promotion-strategy.js @@ -0,0 +1,71 @@ +class PromotionStrategy { + /* Display Firefox Account onboarding dialog in header below signin button */ + shouldShowOnboardingDialog(isOwner, isShotPage) { + let show = false; + // Exit without showing dialog on non-owner shot page + if (isShotPage && !isOwner) { + return show; + } + + const count = localStorage.hasSeenOnboardingDialog; + if (!count) { + localStorage.hasSeenOnboardingDialog = 1; + show = true; + } else if (count < 3) { + localStorage.hasSeenOnboardingDialog = parseInt(count, 10) + 1; + show = true; + } + return show; + } + + /* Display edit tool promotion on shot page below edit button */ + shouldShowEditToolPromotion(isOwner, enableAnnotations, hasFxaOnboardingDialog) { + // Hide edit tool promotion when showing fxaOnboarding dialog + if (!isOwner || !enableAnnotations || hasFxaOnboardingDialog) { + return false; + } + let show = false; + const count = localStorage.hasSeenPromoDialog; + if (!count) { + localStorage.hasSeenPromoDialog = 1; + show = true; + } else if (count < 3) { + localStorage.hasSeenPromoDialog = parseInt(count, 10) + 1; + show = true; + } + return show; + } + + /* Highlight edit tool icon on the shot page when user lands on shot page + for the first time and when the edit tool promo is displayed */ + shouldHighlightEditIcon(isOwner, enableAnnotations) { + if (!isOwner) { + return false; + } + const hasSeen = localStorage.hasSeenEditButton; + if (!hasSeen && enableAnnotations) { + localStorage.hasSeenEditButton = "1"; + } + return !hasSeen; + } + + /* Display upsell ad-banner inside header */ + shouldShowFirefoxBanner(shouldGetFirefox, isOwner) { + if (shouldGetFirefox && !isOwner) { + return true; + } + return false; + } + + /* Display FxA signin ad-banner inside header */ + shouldShowFxaBanner(hasFxa) { + if (!hasFxa) { + return true; + } + return false; + } +} + +if (typeof exports !== "undefined") { + exports.PromotionStrategy = PromotionStrategy; +} diff --git a/server/src/signin-button.js b/server/src/signin-button.js index 6340b5260f..894ab08bc8 100644 --- a/server/src/signin-button.js +++ b/server/src/signin-button.js @@ -2,39 +2,63 @@ const React = require("react"); const PropTypes = require("prop-types"); const { Localized } = require("fluent-react/compat"); const sendEvent = require("./browser-send-event.js"); +const { FxaOnboardingDialog } = require("./fxa-onboarding-dialog"); exports.SignInButton = class SignInButton extends React.Component { constructor(props) { super(props); this.state = { displaySettings: props.isFxaAuthenticated, + hasFxaOnboardingDialog: props.hasFxaOnboardingDialog, }; } static getDerivedStateFromProps(nextProps, prevState) { - return { displaySettings: nextProps.isFxaAuthenticated }; + return { displaySettings: nextProps.isFxaAuthenticated}; + } + + componentDidUpdate(oldProps, oldState) { + if (oldProps.hasFxaOnboardingDialog !== this.props.hasFxaOnboardingDialog) { + this.setState({hasFxaOnboardingDialog: this.props.hasFxaOnboardingDialog}); + } } render() { if (this.state.displaySettings) { - return - - - - ; + return
+ + + + + +
; } const logInURI = "/api/fxa-oauth/login/" + this.props.initialPage; - return - - - - ; + return
+ + + + + + +
; } clickHandler(event) { sendEvent("fxa-signin", this.props.initialPage, {useBeacon: true}); + if (this.state.hasFxaOnboardingDialog) { + this.hideFxaOnboardingDialog(); + } + } + + hideFxaOnboardingDialog() { + const hasFxaOnboardingDialog = !this.state.hasFxaOnboardingDialog; + this.setState({hasFxaOnboardingDialog}); + // set counter to max to stop showing promo again + localStorage.hasSeenOnboardingDialog = 3; } }; @@ -42,4 +66,5 @@ exports.SignInButton.propTypes = { initialPage: PropTypes.string, isFxaAuthenticated: PropTypes.bool, staticLink: PropTypes.func, + hasFxaOnboardingDialog: PropTypes.bool, }; diff --git a/static/css/home.scss b/static/css/home.scss index 0c534449e0..aead4b4f08 100644 --- a/static/css/home.scss +++ b/static/css/home.scss @@ -53,7 +53,6 @@ body { background-size: 400px auto; flex: 0 0 420px; height: 440px; - z-index: 999; } .banner-image-back { @@ -141,7 +140,7 @@ h2 { width: 260px; } -.button.primary { +.button.primary.download-firefox { background: #30e60b; color: #fff; text-align: start; diff --git a/static/css/partials/_fxa-onboarding-dialog.scss b/static/css/partials/_fxa-onboarding-dialog.scss new file mode 100644 index 0000000000..dfd4127324 --- /dev/null +++ b/static/css/partials/_fxa-onboarding-dialog.scss @@ -0,0 +1,110 @@ +.onboarding-promo-dialog { + background: #f7f7f7; + border: 1px solid $light-border; + border-radius: 3px; + box-shadow: $medium-box-shadow; + color: #0c0c0d; + min-height: 200px; + position: absolute; + inset-inline-start: 3px; + top: 47px; + z-index: 2; + + .triangle { + position: absolute; + width: 0; + height: 0; + border-inline-start: 9px solid transparent; + border-inline-end: 9px solid transparent; + border-bottom: 15px solid $light-border; + top: -15px; + inset-inline-start: 8px; + + .triangle-inner { + position: absolute; + width: 0; + height: 0; + border-inline-start: 8px solid transparent; + border-inline-end: 8px solid transparent; + border-bottom: 13px solid $white; + inset-inline-start: -8px; + top: 2px; + } + } + + &.right-align { + top: 70px; + width: 360px; + inset-inline-start: -300px; + + @include respond-to("large") { + top: 85px; + width: 400px; + inset-inline-start: -310px; + } + + .triangle { + inset-inline-start: 325px; + @include respond-to("large") { + inset-inline-start: 347px; + } + } + } + + .promo-header { + display: flex; + align-items: center; + height: 60px; + background: $white; + + .promo-logo { + background-position: center; + background-image: url("../img/icon-promo.svg"); + background-repeat: no-repeat; + background-size: 30px auto; + background-color: #0684ff; + width: 60px; + height: 100%; + display: block; + } + + .promo-title { + display: block; + font-size: 16px; + font-weight: bold; + padding: 0 16px; + } + + } + + .promo-message { + padding: 0 16px; + font-size: 14px; + + ul { + padding-left: 16px; + } + } + + .promo-footer { + height: 70px; + display: flex; + justify-content: flex-end; + align-items: center; + + a { + font-weight: bold; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + + button { + margin: 0 16px; + width: 120px; + height: 40px; + } + } +} diff --git a/static/css/partials/_header.scss b/static/css/partials/_header.scss index 6fb6492678..0d779323e3 100644 --- a/static/css/partials/_header.scss +++ b/static/css/partials/_header.scss @@ -56,6 +56,10 @@ margin-inline-end: 0; } + .fxa-signin { + position: relative; + } + a.nav-button { &:focus { outline: 1px dotted $black; diff --git a/static/css/partials/_partials.scss b/static/css/partials/_partials.scss index eeb541a98a..6eb4eac87e 100644 --- a/static/css/partials/_partials.scss +++ b/static/css/partials/_partials.scss @@ -12,3 +12,4 @@ @import "share-panel"; @import "theme"; @import "header"; +@import "fxa-onboarding-dialog"; diff --git a/static/img/icon-promo.svg b/static/img/icon-promo.svg new file mode 100644 index 0000000000..2ff99b3b99 --- /dev/null +++ b/static/img/icon-promo.svg @@ -0,0 +1 @@ + \ No newline at end of file