From 66cfce7d79a1cedc619cd875605ca5e8e93061bc Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Mon, 16 Dec 2024 11:57:59 +0100 Subject: [PATCH 01/21] feat: Add feature flag for landing page redesign --- .../(public)/LandingViewRedesign.tsx | 18 +++++++++++ .../(redesign)/(public)/page.tsx | 31 ++++++++++++++----- src/db/tables/featureFlags.ts | 1 + 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx new file mode 100644 index 00000000000..fb341e9edd6 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; +import { ExtendedReactLocalization } from "../../../functions/l10n"; + +export type Props = { + countryCode: string; + eligibleForPremium: boolean; + experimentData: ExperimentData; + l10n: ExtendedReactLocalization; + scanLimitReached: boolean; +}; + +export const View = (props: Props) => { + return

{props.l10n.getString("landing-all-hero-title")}

; +}; diff --git a/src/app/(proper_react)/(redesign)/(public)/page.tsx b/src/app/(proper_react)/(redesign)/(public)/page.tsx index b05e69286d9..fa9c7f8a243 100644 --- a/src/app/(proper_react)/(redesign)/(public)/page.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/page.tsx @@ -12,7 +12,8 @@ import { } from "../../../functions/server/onerep"; import { isEligibleForPremium } from "../../../functions/universal/premium"; import { getL10n } from "../../../functions/l10n/serverComponents"; -import { View } from "./LandingView"; +import { View as LandingView } from "./LandingView"; +import { View as LandingViewRedesign } from "./LandingViewRedesign"; import { CONST_DAY_MILLISECONDS, CONST_URL_MONITOR_LANDING_PAGE_ID, @@ -21,6 +22,7 @@ import { getExperimentationId } from "../../../functions/server/getExperimentati import { getExperiments } from "../../../functions/server/getExperiments"; import { getLocale } from "../../../functions/universal/getLocale"; import { AccountsMetricsFlowProvider } from "../../../../contextProviders/accounts-metrics-flow"; +import { getEnabledFeatureFlags } from "../../../../db/tables/featureFlags"; type Props = { searchParams: { @@ -36,6 +38,9 @@ export default async function Page({ searchParams }: Props) { const countryCode = getCountryCode(headers()); const eligibleForPremium = isEligibleForPremium(countryCode); + const enabledFeatureFlags = await getEnabledFeatureFlags({ + isSignedOut: true, + }); const experimentationId = getExperimentationId(session?.user ?? null); const experimentData = await getExperiments({ experimentationId, @@ -68,13 +73,23 @@ export default async function Page({ searchParams }: Props) { service: process.env.OAUTH_CLIENT_ID as string, }} > - + {enabledFeatureFlags.includes("LandingPageRedesign") ? ( + + ) : ( + + )} ); } diff --git a/src/db/tables/featureFlags.ts b/src/db/tables/featureFlags.ts index 381b634aff5..adea5b74836 100644 --- a/src/db/tables/featureFlags.ts +++ b/src/db/tables/featureFlags.ts @@ -55,6 +55,7 @@ export const featureFlagNames = [ "DataBrokerRemovalTimeEstimateLabel", "DataBrokerRemovalTimeEstimateCsat", "SettingsPageRedesign", + "LandingPageRedesign", ] as const; export type FeatureFlagName = (typeof featureFlagNames)[number]; From 4ae5d63e0e93ddb4442897f5ed0c7dc30f3e4436 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Mon, 16 Dec 2024 12:13:09 +0100 Subject: [PATCH 02/21] feat: Add experiment for landing page redesign --- config/nimbus.yaml | 34 +++++++++++++++++++ .../(redesign)/(public)/page.tsx | 4 ++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/config/nimbus.yaml b/config/nimbus.yaml index 60cb8573ac1..43805d177a2 100644 --- a/config/nimbus.yaml +++ b/config/nimbus.yaml @@ -101,6 +101,33 @@ features: value: { "enabled": true } - channel: production value: { "enabled": true } + landing-page-redesign: + description: Landing page redesign + variables: + enabled: + description: If the feature is enabled + type: Boolean + default: false + variant: + description: The landing page variant to show + type: LandingPageVariant + default: default + defaults: + - channel: local + value: { + "enabled": true, + "variant": redesign, + } + - channel: staging + value: { + "enabled": false, + "variant": default, + } + - channel: production + value: { + "enabled": false, + "variant": default, + } enums: OptionalBrokerScanInfoFields: description: An enum of optional broker scan info fields @@ -122,3 +149,10 @@ enums: description: Only show a CTA button with the label “Get free scan” ctaOnlyAlternativeLabel: description: Only show a CTA button with the label “Sign up to get free scan” + LandingPageVariant: + description: An enum of landing page variants + variants: + default: + description: Show the default landing page + redesign: + description: Show the redesigned landing page diff --git a/src/app/(proper_react)/(redesign)/(public)/page.tsx b/src/app/(proper_react)/(redesign)/(public)/page.tsx index fa9c7f8a243..25094a985ab 100644 --- a/src/app/(proper_react)/(redesign)/(public)/page.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/page.tsx @@ -73,7 +73,9 @@ export default async function Page({ searchParams }: Props) { service: process.env.OAUTH_CLIENT_ID as string, }} > - {enabledFeatureFlags.includes("LandingPageRedesign") ? ( + {enabledFeatureFlags.includes("LandingPageRedesign") && + experimentData["landing-page-redesign"].enabled && + experimentData["landing-page-redesign"].variant === "redesign" ? ( Date: Mon, 16 Dec 2024 14:54:49 +0100 Subject: [PATCH 03/21] chore: Add landing page menu --- .../(public)/LandingView.stories.tsx | 6 +- .../(redesign)/(public)/LandingView.tsx | 431 ++++++++---------- .../(public)/LandingViewRedesign.module.scss | 7 + .../(public)/LandingViewRedesign.tsx | 9 +- .../(public)/PublicShell.module.scss | 24 +- .../(redesign)/(public)/PublicShell.tsx | 67 ++- .../(redesign)/(public)/TopNavBar.tsx | 59 +++ .../[breachName]/BreachDetailView.stories.tsx | 2 +- .../breaches/BreachIndexView.stories.tsx | 2 +- .../how-it-works/HowItWorksView.stories.tsx | 2 +- .../(redesign)/(public)/layout.tsx | 6 +- .../(redesign)/(public)/page.tsx | 2 + .../(redesign)/MobileShell.module.scss | 47 +- .../(proper_react)/(redesign)/MobileShell.tsx | 248 +++++----- src/app/components/client/SignInButton.tsx | 5 +- 15 files changed, 510 insertions(+), 407 deletions(-) create mode 100644 src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.module.scss create mode 100644 src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx index a92d5ecd68a..8ada09d68c0 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx @@ -30,7 +30,11 @@ const meta: Meta = { service: process.env.OAUTH_CLIENT_ID as string, }} > - + diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx index 4d3b9bfa8df..fae4810800a 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx @@ -26,9 +26,9 @@ import { TelemetryLink } from "../../../components/client/TelemetryLink"; import { HeresHowWeHelp } from "./HeresHowWeHelp"; import { ScanLimit } from "./ScanLimit"; import { FaqSection } from "./Faq"; -import { AccountDeletionNotification } from "./AccountDeletionNotification"; import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; import { FreeScanCta } from "./FreeScanCta"; +import { TopNavBar } from "./TopNavBar"; export type Props = { eligibleForPremium: boolean; @@ -40,267 +40,218 @@ export type Props = { export const View = (props: Props) => { return ( - <> - -
- {props.eligibleForPremium && } -
-
-

{props.l10n.getString("landing-all-hero-title")}

-

- {props.l10n.getString( - props.eligibleForPremium - ? "landing-premium-hero-lead" - : "landing-all-hero-lead", - )} -

- {props.eligibleForPremium && props.scanLimitReached ? ( - - ) : ( - +
+ {props.eligibleForPremium && } +
+
+

{props.l10n.getString("landing-all-hero-title")}

+

+ {props.l10n.getString( + props.eligibleForPremium + ? "landing-premium-hero-lead" + : "landing-all-hero-lead", )} -

-
- -
-
+

+ {props.eligibleForPremium && props.scanLimitReached ? ( + + ) : ( + + )} +
+
+ +
+
-
-
+
+
+

+ {props.eligibleForPremium + ? props.l10n.getFragment("landing-premium-quote", { + elems: { + data_brokers: , + }, + }) + : props.l10n.getFragment("landing-all-quote", { + elems: { + data_breaches: , + }, + })} +

+
+
+ +
+
+

+ {props.l10n.getString("landing-all-value-prop-fix-exposures")} +

+

{props.eligibleForPremium - ? props.l10n.getFragment("landing-premium-quote", { - elems: { - data_brokers: , + ? props.l10n.getFragment( + "landing-premium-value-prop-fix-exposures-description", + { + elems: { + privacy_link: ( + + ), + }, }, - }) - : props.l10n.getFragment("landing-all-quote", { - elems: { - data_breaches: , + ) + : props.l10n.getFragment( + "landing-all-value-prop-fix-exposures-description", + { + elems: { + privacy_link: ( + + ), + }, }, - })} - + )} +

+ +
+
+ {props.eligibleForPremium ? ( + {props.l10n.getString( + ) : ( + + )}
-
-
- -

- {props.l10n.getString("landing-all-value-prop-fix-exposures")} -

-

- {props.eligibleForPremium - ? props.l10n.getFragment( - "landing-premium-value-prop-fix-exposures-description", - { - elems: { - privacy_link: ( - - ), - }, - }, - ) - : props.l10n.getFragment( - "landing-all-value-prop-fix-exposures-description", - { - elems: { - privacy_link: ( - - ), - }, +

+ +

+ {props.l10n.getString("landing-all-value-prop-info-at-risk")} +

+

+ {props.eligibleForPremium + ? props.l10n.getFragment( + "landing-premium-value-prop-info-at-risk-description", + { + elems: { + exposure_type_list: ( + + ), }, - )} -

- -
-
- {props.eligibleForPremium ? ( - {props.l10n.getString( - ) : ( - - )} -
-
- -
- -

- {props.l10n.getString("landing-all-value-prop-info-at-risk")} -

-

- {props.eligibleForPremium - ? props.l10n.getFragment( - "landing-premium-value-prop-info-at-risk-description", - { - elems: { - exposure_type_list: ( - - ), - }, - }, - ) - : props.l10n.getString( - "landing-all-value-prop-info-at-risk-description", - )} -

- -
-
- -
+

+ + +
+
+
-
-

- {props.l10n.getString("landing-all-get-started")} -

- -
+
+

+ {props.l10n.getString("landing-all-get-started")} +

+ +
-
-

- {props.l10n.getString("landing-all-social-proof-title", { - num_users: 10, - })} -

-

- {props.l10n.getString("landing-all-social-proof-description", { - num_countries: 237, - })} +

+

+ {props.l10n.getString("landing-all-social-proof-title", { + num_users: 10, + })} +

+

+ {props.l10n.getString("landing-all-social-proof-description", { + num_countries: 237, + })} +

+
+

+ {props.l10n.getString("landing-all-social-proof-press")}

-
-

- {props.l10n.getString("landing-all-social-proof-press")} -

- Forbes - Tech Crunch - PC Magazine - CNET - Google -
+ Forbes + Tech Crunch + PC Magazine + CNET + Google
+
- {!props.eligibleForPremium && } - + {!props.eligibleForPremium && } + - + -
-

- {props.l10n.getString("landing-all-take-back-data")} -

- -
-
- - ); -}; - -export const TopNavBar = ({ l10n }: { l10n: ExtendedReactLocalization }) => { - return ( -
-
- +

+ {props.l10n.getString("landing-all-take-back-data")} +

+ - {l10n.getString("landing-premium-hero-navbar-link-how-it-works")} -
- - {l10n.getString("landing-premium-hero-navbar-link-pricing")} - - - {l10n.getString("landing-premium-hero-navbar-link-faqs")} - - - {l10n.getString("landing-premium-hero-navbar-link-recent-breaches")} - + scanLimitReached={props.scanLimitReached} + experimentData={props.experimentData} + />
-
+ ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.module.scss b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.module.scss new file mode 100644 index 00000000000..a56803ec8bf --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.module.scss @@ -0,0 +1,7 @@ +@import "../../../tokens"; + +.wrapper { + display: flex; + flex-direction: column; + height: 100%; +} diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx index fb341e9edd6..9476c0e2c29 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx @@ -4,6 +4,7 @@ import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; import { ExtendedReactLocalization } from "../../../functions/l10n"; +import styles from "./LandingViewRedesign.module.scss"; export type Props = { countryCode: string; @@ -14,5 +15,11 @@ export type Props = { }; export const View = (props: Props) => { - return

{props.l10n.getString("landing-all-hero-title")}

; + return ( +
+
+

{props.l10n.getString("landing-all-hero-title")}

+
+
+ ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/PublicShell.module.scss b/src/app/(proper_react)/(redesign)/(public)/PublicShell.module.scss index e890a27dc6d..9ca660cf32a 100644 --- a/src/app/(proper_react)/(redesign)/(public)/PublicShell.module.scss +++ b/src/app/(proper_react)/(redesign)/(public)/PublicShell.module.scss @@ -26,7 +26,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: $spacing-md; + padding: $spacing-lg $layout-xl; background-color: $color-white; gap: $spacing-lg; @@ -44,11 +44,29 @@ flex-shrink: 0; } - @media screen and (min-width: $screen-md) { - padding: $spacing-lg $layout-xl; + @media screen and (max-width: $screen-xl) { + display: none; } } .content { flex: 1 0 auto; } + +.navbar { + display: flex; + flex-direction: row; + font-weight: 600; + justify-content: flex-start; + + .navbarLinksContainer { + display: flex; + gap: $spacing-md; + } + + .navbarLinks { + color: $color-black; + padding: $spacing-sm; + text-decoration: none; + } +} diff --git a/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx b/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx index b697d76c792..2864cd41f3c 100644 --- a/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx @@ -12,38 +12,57 @@ import { SignInButton } from "../../../components/client/SignInButton"; import { Footer } from "../Footer"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import { MobileShell } from "../MobileShell"; +import { + getPremiumSubscriptionUrl, + getSubscriptionBillingAmount, +} from "../../../functions/server/getPremiumSubscriptionInfo"; +import { FeatureFlagName } from "../../../../db/tables/featureFlags"; +import { TopNavBar } from "./TopNavBar"; export type Props = { children: ReactNode; l10n: ExtendedReactLocalization; countryCode: string; + enabledFeatureFlags: FeatureFlagName[]; }; export const PublicShell = (props: Props) => { return ( -
- -
- -
-
{props.children}
-
-
+ +
+ +
+ +
+
{props.children}
+
+
+
); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx b/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx new file mode 100644 index 00000000000..922cc1ac2a2 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use client"; + +import { TelemetryLink } from "../../../components/client/TelemetryLink"; +import { useL10n } from "../../../hooks/l10n"; + +export const TopNavBar = (props: { + styles: { + readonly [key: string]: string; + }; +}) => { + const l10n = useL10n(); + return ( +
+
+ + {l10n.getString("landing-premium-hero-navbar-link-how-it-works")} + + + {l10n.getString("landing-premium-hero-navbar-link-pricing")} + + + {l10n.getString("landing-premium-hero-navbar-link-faqs")} + + + {l10n.getString("landing-premium-hero-navbar-link-recent-breaches")} + +
+
+ ); +}; diff --git a/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx index 2adf3435325..da80145fbaf 100644 --- a/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx @@ -11,7 +11,7 @@ import { createRandomHibpListing } from "../../../../../../apiMocks/mockData"; const meta: Meta = { title: "Pages/Public/Breach listing", component: (props: ViewProps) => ( - + ), diff --git a/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx index de5b7cc67b2..dfaebe61448 100644 --- a/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx @@ -12,7 +12,7 @@ import { createRandomHibpListing } from "../../../../../apiMocks/mockData"; const meta: Meta = { title: "Pages/Public/Breach index", component: (props: ViewProps) => ( - + ), diff --git a/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx index 955d62293e6..748e90f3cf5 100644 --- a/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx @@ -10,7 +10,7 @@ import { PublicShell } from "../PublicShell"; const meta: Meta = { title: "Pages/Public/HowItWorks page", component: () => ( - + + {props.children} ); diff --git a/src/app/(proper_react)/(redesign)/(public)/page.tsx b/src/app/(proper_react)/(redesign)/(public)/page.tsx index 25094a985ab..2f3ba51873f 100644 --- a/src/app/(proper_react)/(redesign)/(public)/page.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/page.tsx @@ -23,6 +23,7 @@ import { getExperiments } from "../../../functions/server/getExperiments"; import { getLocale } from "../../../functions/universal/getLocale"; import { AccountsMetricsFlowProvider } from "../../../../contextProviders/accounts-metrics-flow"; import { getEnabledFeatureFlags } from "../../../../db/tables/featureFlags"; +import { AccountDeletionNotification } from "./AccountDeletionNotification"; type Props = { searchParams: { @@ -73,6 +74,7 @@ export default async function Page({ searchParams }: Props) { service: process.env.OAUTH_CLIENT_ID as string, }} > + {enabledFeatureFlags.includes("LandingPageRedesign") && experimentData["landing-page-redesign"].enabled && experimentData["landing-page-redesign"].variant === "redesign" ? ( diff --git a/src/app/(proper_react)/(redesign)/MobileShell.module.scss b/src/app/(proper_react)/(redesign)/MobileShell.module.scss index 418e76d18c6..2959a208197 100644 --- a/src/app/(proper_react)/(redesign)/MobileShell.module.scss +++ b/src/app/(proper_react)/(redesign)/MobileShell.module.scss @@ -12,10 +12,10 @@ @media screen and (max-width: $screen-xl) { position: sticky; background-color: $color-white; - box-shadow: $box-shadow-sm; width: 100%; display: flex; align-items: center; + justify-content: space-between; gap: $spacing-sm; padding: $spacing-xs $spacing-sm; // Overlay `.nonHeader` so that the box-shadow of this element @@ -23,43 +23,36 @@ z-index: 1; .headerStart { - flex: 0 0 20%; display: flex; align-items: center; justify-content: flex-start; - - .menuToggleButton { - display: flex; - align-items: center; - justify-content: center; - background-color: transparent; - border-style: none; - padding: $spacing-md; - cursor: pointer; - - svg { - width: auto; - height: 16px; - } - - &:hover svg { - color: $color-blue-50; - } - } } - .headerMiddle { - flex: 1 0 auto; + .headerEnd { + align-self: flex-end; display: flex; align-items: center; - justify-content: center; + gap: $spacing-sm; + justify-content: flex-end; } - .headerEnd { - flex: 0 0 20%; + .menuToggleButton { display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; + background-color: transparent; + border-style: none; + padding: $spacing-md; + cursor: pointer; + + svg { + width: auto; + height: 16px; + } + + &:hover svg { + color: $color-blue-50; + } } } } diff --git a/src/app/(proper_react)/(redesign)/MobileShell.tsx b/src/app/(proper_react)/(redesign)/MobileShell.tsx index a09c5d773a8..18d8f48cfd4 100644 --- a/src/app/(proper_react)/(redesign)/MobileShell.tsx +++ b/src/app/(proper_react)/(redesign)/MobileShell.tsx @@ -19,10 +19,12 @@ import { useTelemetry } from "../../hooks/useTelemetry"; import { usePathname } from "next/navigation"; import { CONST_SETTINGS_TAB_SLUGS } from "../../../constants"; import { FeatureFlagName } from "../../../db/tables/featureFlags"; +import { SignInButton } from "../../components/client/SignInButton"; +import { TelemetryLink } from "../../components/client/TelemetryLink"; export type Props = { countryCode: string; - session: Session; + session: Session | null; monthlySubscriptionUrl: string; yearlySubscriptionUrl: string; subscriptionBillingAmount: { @@ -50,7 +52,6 @@ export const MobileShell = (props: Props) => { // As we transition focus away from the navigation bar in deeper sections // of the experience, it's best to ensure its focus on the dashboard page, // where users first encounter it and when they return to it - /* c8 ignore next 3 */ if (isOnDashboard) { wrapperRef.current?.focus(); } @@ -59,45 +60,13 @@ export const MobileShell = (props: Props) => { return (
- -
-
{
- + {props.session ? ( + + ) : ( + + )} +
@@ -127,76 +117,124 @@ export const MobileShell = (props: Props) => { aria-label={l10n.getString("mobile-menu-label")} >
-
    -
  • - +
      +
    • + + {l10n.getString("main-nav-link-dashboard-label")} + +
    • + {props.countryCode === "us" && ( +
    • + + {l10n.getString("main-nav-link-how-it-works-label")} + +
    • + )} +
    • + + {l10n.getString("main-nav-link-faq-label")} + +
    • +
    • + + {l10n.getString("main-nav-link-settings-label")} + + {props.enabledFeatureFlags.includes( + "SettingsPageRedesign", + ) && ( +
        + {CONST_SETTINGS_TAB_SLUGS.map((submenuKey) => { + return ( +
      • + + {l10n.getString( + `settings-tab-label-${submenuKey}`, + )} + +
      • + ); + })} +
      + )} +
    • +
    +
    + +
    + + ) : ( +
      + - {l10n.getString("main-nav-link-dashboard-label")} - - - {props.countryCode === "us" && ( -
    • - - {l10n.getString("main-nav-link-how-it-works-label")} - -
    • - )} -
    • - + - {l10n.getString("main-nav-link-faq-label")} - -
    • -
    • - + - {l10n.getString("main-nav-link-settings-label")} - - {props.enabledFeatureFlags.includes("SettingsPageRedesign") && ( -
        - {CONST_SETTINGS_TAB_SLUGS.map((submenuKey) => { - return ( -
      • - - {l10n.getString(`settings-tab-label-${submenuKey}`)} - -
      • - ); - })} -
      - )} -
    • -
    -
    - -
    + {l10n.getString("landing-premium-hero-navbar-link-faqs")} + + + {l10n.getString( + "landing-premium-hero-navbar-link-recent-breaches", + )} + +
+ )}
{props.children}
diff --git a/src/app/components/client/SignInButton.tsx b/src/app/components/client/SignInButton.tsx index b73d888eb9a..cdfb2f199e8 100644 --- a/src/app/components/client/SignInButton.tsx +++ b/src/app/components/client/SignInButton.tsx @@ -6,10 +6,10 @@ import { signIn, useSession } from "next-auth/react"; import { useL10n } from "../../hooks/l10n"; -import { Button } from "./Button"; +import { Button, ButtonProps } from "./Button"; import { useTelemetry } from "../../hooks/useTelemetry"; -export const SignInButton = () => { +export const SignInButton = (props: ButtonProps) => { const l10n = useL10n(); const session = useSession(); const recordTelemetry = useTelemetry(); @@ -20,6 +20,7 @@ export const SignInButton = () => { return (
diff --git a/src/app/(proper_react)/(redesign)/MobileShell.module.scss b/src/app/(proper_react)/(redesign)/MobileShell.module.scss index 2959a208197..b35aa7f9032 100644 --- a/src/app/(proper_react)/(redesign)/MobileShell.module.scss +++ b/src/app/(proper_react)/(redesign)/MobileShell.module.scss @@ -26,6 +26,7 @@ display: flex; align-items: center; justify-content: flex-start; + padding-left: $spacing-md; } .headerEnd { @@ -51,7 +52,7 @@ } &:hover svg { - color: $color-blue-50; + color: $color-purple-70; } } } @@ -78,6 +79,10 @@ display: none; } + .navbar { + width: 100%; + } + @media screen and (max-width: $screen-xl) { .hasOpenMenu & .mainMenuLayer { display: block; @@ -95,62 +100,53 @@ flex-direction: column; ul { + border-bottom: 1px solid $color-grey-10; list-style-type: none; padding: 0; width: 100%; - } - - a, - a:visited { - display: block; - padding: $spacing-sm $spacing-lg; - color: $color-grey-40; - font-weight: 500; - border-top: 1px solid $color-grey-10; - text-decoration: none; - - &.isActive { - color: $color-purple-70; - } - - &:hover { - background-color: $color-purple-50; - color: $color-white; - text-decoration: underline; - } - - // The `a` and `a:visited` violate this rule, but are safe: - // stylelint-disable-next-line no-descending-specificity - &:focus { - background-color: $color-blue-50; - color: $color-white; - outline: none; - } - } - .subMenu { - padding-left: $layout-xs; - - // The `a` and `a:visited` violate this rule, but are safe: - // stylelint-disable-next-line no-descending-specificity - a, - a:visited { - border-top: none; + a { + align-items: center; + display: flex; + padding: $spacing-md $spacing-xl; + gap: $spacing-sm; + color: $color-black; + font-weight: 700; + border-top: 1px solid $color-grey-10; + text-decoration: none; &.isActive { - color: $color-purple-40; + color: $color-purple-70; text-decoration: underline; - - &:hover { - color: $color-purple-70; - } } &:hover { - color: $color-grey-40; - background: none; + background-color: $color-grey-10; + color: $color-purple-70; + } + + // The `a` and `a:visited` violate this rule, but are safe: + // stylelint-disable-next-line no-descending-specificity + &:focus { + background-color: $color-grey-10; + outline: none; text-decoration: underline; } + + & > svg { + fill: $color-purple-70; + } + } + + &.subMenu { + border-bottom: none; + border-top: 1px solid $color-grey-10; + font: $text-body-md; + + a { + padding: $spacing-md $spacing-2xl; + border-top: none; + } } } } diff --git a/src/app/(proper_react)/(redesign)/MobileShell.tsx b/src/app/(proper_react)/(redesign)/MobileShell.tsx index 18d8f48cfd4..1ede098a7f6 100644 --- a/src/app/(proper_react)/(redesign)/MobileShell.tsx +++ b/src/app/(proper_react)/(redesign)/MobileShell.tsx @@ -11,7 +11,11 @@ import { Session } from "next-auth"; import styles from "./MobileShell.module.scss"; import monitorLogo from "../images/monitor-logo.webp"; import { UpsellBadge } from "../../components/client/toolbar/UpsellBadge"; -import { CloseBigIcon, ListIcon } from "../../components/server/Icons"; +import { + CaretRight, + CloseBigIcon, + ListIcon, +} from "../../components/server/Icons"; import { UserMenu } from "../../components/client/toolbar/UserMenu"; import { useL10n } from "../../hooks/l10n"; import { PageLink } from "./PageLink"; @@ -20,7 +24,7 @@ import { usePathname } from "next/navigation"; import { CONST_SETTINGS_TAB_SLUGS } from "../../../constants"; import { FeatureFlagName } from "../../../db/tables/featureFlags"; import { SignInButton } from "../../components/client/SignInButton"; -import { TelemetryLink } from "../../components/client/TelemetryLink"; +import { TopNavBar } from "./(public)/TopNavBar"; export type Props = { countryCode: string; @@ -126,7 +130,10 @@ export const MobileShell = (props: Props) => { activeClassName={styles.isActive} hasTelemetry={{ link_id: "navigation_dashboard" }} > - {l10n.getString("main-nav-link-dashboard-label")} + <> + + {l10n.getString("main-nav-link-dashboard-label")} + {props.countryCode === "us" && ( @@ -137,7 +144,10 @@ export const MobileShell = (props: Props) => { target="_blank" hasTelemetry={{ link_id: "navigation_how_it_works" }} > - {l10n.getString("main-nav-link-how-it-works-label")} + <> + + {l10n.getString("main-nav-link-how-it-works-label")} + )} @@ -148,7 +158,10 @@ export const MobileShell = (props: Props) => { target="_blank" hasTelemetry={{ link_id: "navigation_faq" }} > - {l10n.getString("main-nav-link-faq-label")} + <> + + {l10n.getString("main-nav-link-faq-label")} +
  • @@ -157,7 +170,10 @@ export const MobileShell = (props: Props) => { activeClassName={styles.isActive} hasTelemetry={{ link_id: "navigation_settings" }} > - {l10n.getString("main-nav-link-settings-label")} + <> + + {l10n.getString("main-nav-link-settings-label")} + {props.enabledFeatureFlags.includes( "SettingsPageRedesign", @@ -173,9 +189,12 @@ export const MobileShell = (props: Props) => { link_id: `navigation_settings_${submenuKey}`, }} > - {l10n.getString( - `settings-tab-label-${submenuKey}`, - )} + <> + + {l10n.getString( + `settings-tab-label-${submenuKey}`, + )} +
  • ); @@ -195,45 +214,7 @@ export const MobileShell = (props: Props) => { ) : ( -
      - - {l10n.getString( - "landing-premium-hero-navbar-link-how-it-works", - )} - - - {l10n.getString("landing-premium-hero-navbar-link-pricing")} - - - {l10n.getString("landing-premium-hero-navbar-link-faqs")} - - - {l10n.getString( - "landing-premium-hero-navbar-link-recent-breaches", - )} - -
    + )} diff --git a/src/app/components/server/Icons.tsx b/src/app/components/server/Icons.tsx index 1ba3a0954de..9c6ae73f821 100644 --- a/src/app/components/server/Icons.tsx +++ b/src/app/components/server/Icons.tsx @@ -688,3 +688,25 @@ export const BackArrow = (props: SVGProps & { alt: string }) => { ); }; + +// Keywords: caret, right +export const CaretRight = ( + props: SVGProps & { alt: string }, +) => { + return ( + + {props.alt && {props.alt}} + + + ); +}; From ca4ea5f1aadda345605b19078c1ef906c5568c26 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Tue, 17 Dec 2024 12:21:23 +0100 Subject: [PATCH 05/21] fix: Only show MobileShell when landing page redesign is enabled --- .../(public)/LandingView.stories.tsx | 9 +- .../(redesign)/(public)/LandingView.tsx | 388 +++++++++--------- .../(public)/LandingViewRedesign.tsx | 14 +- .../(redesign)/(public)/PublicShell.tsx | 34 +- .../(redesign)/(public)/TopNavBar.tsx | 112 ++--- .../[breachName]/BreachDetailView.stories.tsx | 14 +- .../breaches/BreachIndexView.stories.tsx | 14 +- .../how-it-works/HowItWorksView.stories.tsx | 14 +- .../(redesign)/(public)/layout.tsx | 14 +- .../(redesign)/(public)/page.tsx | 2 - 10 files changed, 353 insertions(+), 262 deletions(-) diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx index 8ada09d68c0..c0d8faf66ac 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx @@ -32,8 +32,15 @@ const meta: Meta = { > diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx index fae4810800a..3a5091b87d8 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx @@ -29,6 +29,7 @@ import { FaqSection } from "./Faq"; import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; import { FreeScanCta } from "./FreeScanCta"; import { TopNavBar } from "./TopNavBar"; +import { AccountDeletionNotification } from "./AccountDeletionNotification"; export type Props = { eligibleForPremium: boolean; @@ -40,218 +41,221 @@ export type Props = { export const View = (props: Props) => { return ( -
    - {props.eligibleForPremium && } -
    -
    -

    {props.l10n.getString("landing-all-hero-title")}

    -

    - {props.l10n.getString( - props.eligibleForPremium - ? "landing-premium-hero-lead" - : "landing-all-hero-lead", + <> + +

    + {props.eligibleForPremium && } +
    +
    +

    {props.l10n.getString("landing-all-hero-title")}

    +

    + {props.l10n.getString( + props.eligibleForPremium + ? "landing-premium-hero-lead" + : "landing-all-hero-lead", + )} +

    + {props.eligibleForPremium && props.scanLimitReached ? ( + + ) : ( + )} -

    - {props.eligibleForPremium && props.scanLimitReached ? ( - - ) : ( - - )} -
    -
    - -
    -
    - -
    -
    -

    - {props.eligibleForPremium - ? props.l10n.getFragment("landing-premium-quote", { - elems: { - data_brokers: , - }, - }) - : props.l10n.getFragment("landing-all-quote", { - elems: { - data_breaches: , - }, - })} -

    -
    -
    +
    +
    + +
    +
    -
    -
    - +
    +

    - {props.l10n.getString("landing-all-value-prop-fix-exposures")} -

    -

    {props.eligibleForPremium - ? props.l10n.getFragment( - "landing-premium-value-prop-fix-exposures-description", - { - elems: { - privacy_link: ( - - ), - }, + ? props.l10n.getFragment("landing-premium-quote", { + elems: { + data_brokers: , }, - ) - : props.l10n.getFragment( - "landing-all-value-prop-fix-exposures-description", - { - elems: { - privacy_link: ( - - ), - }, + }) + : props.l10n.getFragment("landing-all-quote", { + elems: { + data_breaches: , }, - )} -

    - - -
    - {props.eligibleForPremium ? ( - {props.l10n.getString( - ) : ( - - )} + })} +
    -
    - -

    - {props.l10n.getString("landing-all-value-prop-info-at-risk")} -

    -

    - {props.eligibleForPremium - ? props.l10n.getFragment( - "landing-premium-value-prop-info-at-risk-description", - { - elems: { - exposure_type_list: ( - - ), +

    +
    + +

    + {props.l10n.getString("landing-all-value-prop-fix-exposures")} +

    +

    + {props.eligibleForPremium + ? props.l10n.getFragment( + "landing-premium-value-prop-fix-exposures-description", + { + elems: { + privacy_link: ( + + ), + }, }, - }, - ) - : props.l10n.getString( - "landing-all-value-prop-info-at-risk-description", + ) + : props.l10n.getFragment( + "landing-all-value-prop-fix-exposures-description", + { + elems: { + privacy_link: ( + + ), + }, + }, + )} +

    + +
    +
    + {props.eligibleForPremium ? ( + {props.l10n.getString( - - -
    - + data-testid="progress-card-image" + /> + ) : ( + + )} +
    +
    + +
    + +

    + {props.l10n.getString("landing-all-value-prop-info-at-risk")} +

    +

    + {props.eligibleForPremium + ? props.l10n.getFragment( + "landing-premium-value-prop-info-at-risk-description", + { + elems: { + exposure_type_list: ( + + ), + }, + }, + ) + : props.l10n.getString( + "landing-all-value-prop-info-at-risk-description", + )} +

    + +
    +
    + +
    -
    -
    -

    - {props.l10n.getString("landing-all-get-started")} -

    - -
    +
    +

    + {props.l10n.getString("landing-all-get-started")} +

    + +
    -
    -

    - {props.l10n.getString("landing-all-social-proof-title", { - num_users: 10, - })} -

    -

    - {props.l10n.getString("landing-all-social-proof-description", { - num_countries: 237, - })} -

    -
    -

    - {props.l10n.getString("landing-all-social-proof-press")} +

    +

    + {props.l10n.getString("landing-all-social-proof-title", { + num_users: 10, + })} +

    +

    + {props.l10n.getString("landing-all-social-proof-description", { + num_countries: 237, + })}

    - Forbes - Tech Crunch - PC Magazine - CNET - Google +
    +

    + {props.l10n.getString("landing-all-social-proof-press")} +

    + Forbes + Tech Crunch + PC Magazine + CNET + Google +
    -
    - {!props.eligibleForPremium && } - + {!props.eligibleForPremium && } + - + -
    -

    - {props.l10n.getString("landing-all-take-back-data")} -

    - -
    -
    +
    +

    + {props.l10n.getString("landing-all-take-back-data")} +

    + +
    + + ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx index 9476c0e2c29..ad1778814f4 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx @@ -4,6 +4,7 @@ import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; import { ExtendedReactLocalization } from "../../../functions/l10n"; +import { AccountDeletionNotification } from "./AccountDeletionNotification"; import styles from "./LandingViewRedesign.module.scss"; export type Props = { @@ -16,10 +17,13 @@ export type Props = { export const View = (props: Props) => { return ( -
    -
    -

    {props.l10n.getString("landing-all-hero-title")}

    -
    -
    + <> + +
    +
    +

    {props.l10n.getString("landing-all-hero-title")}

    +
    +
    + ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx b/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx index 2864cd41f3c..2919ab7ca11 100644 --- a/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx @@ -19,25 +19,42 @@ import { } from "../../../functions/server/getPremiumSubscriptionInfo"; import { FeatureFlagName } from "../../../../db/tables/featureFlags"; import { TopNavBar } from "./TopNavBar"; +import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; export type Props = { children: ReactNode; l10n: ExtendedReactLocalization; countryCode: string; enabledFeatureFlags: FeatureFlagName[]; + experimentData: ExperimentData; }; -export const PublicShell = (props: Props) => { +const PublicMobileShell = (props: Props) => { + if ( + !( + props.enabledFeatureFlags.includes("LandingPageRedesign") && + props.experimentData["landing-page-redesign"].enabled && + props.experimentData["landing-page-redesign"].variant === "redesign" + ) + ) { + return props.children; + } + return ( + fxaSettingsUrl={process.env.FXA_SETTINGS_URL!} + /> + ); +}; + +export const PublicShell = (props: Props) => { + return ( +
    { /> - + {props.enabledFeatureFlags.includes("LandingPageRedesign") && + props.experimentData["landing-page-redesign"].enabled && + props.experimentData["landing-page-redesign"].variant === + "redesign" && }
    {props.children}
    -
    + ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx b/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx index f145dd44a6d..ac664e64c08 100644 --- a/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/TopNavBar.tsx @@ -17,57 +17,67 @@ export const TopNavBar = (props: { const l10n = useL10n(); return (
    -
    - - <> - {props.showCaret && } - {l10n.getString("landing-premium-hero-navbar-link-how-it-works")} - - - - <> - {props.showCaret && } - {l10n.getString("landing-premium-hero-navbar-link-pricing")} - - - - <> - {props.showCaret && } - {l10n.getString("landing-premium-hero-navbar-link-faqs")} - - - - <> - {props.showCaret && } - {l10n.getString("landing-premium-hero-navbar-link-recent-breaches")} - - -
    +
      +
    • + + <> + {props.showCaret && } + {l10n.getString("landing-premium-hero-navbar-link-how-it-works")} + + +
    • +
    • + + <> + {props.showCaret && } + {l10n.getString("landing-premium-hero-navbar-link-pricing")} + + +
    • +
    • + + <> + {props.showCaret && } + {l10n.getString("landing-premium-hero-navbar-link-faqs")} + + +
    • +
    • + + <> + {props.showCaret && } + {l10n.getString( + "landing-premium-hero-navbar-link-recent-breaches", + )} + + +
    • +
    ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx index da80145fbaf..6510c6d7104 100644 --- a/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/breach-details/[breachName]/BreachDetailView.stories.tsx @@ -7,11 +7,23 @@ import { BreachDetailsView, Props as ViewProps } from "./BreachDetailView"; import { getL10n } from "../../../../../functions/l10n/storybookAndJest"; import { PublicShell } from "../../PublicShell"; import { createRandomHibpListing } from "../../../../../../apiMocks/mockData"; +import { defaultExperimentData } from "../../../../../../telemetry/generated/nimbus/experiments"; const meta: Meta = { title: "Pages/Public/Breach listing", component: (props: ViewProps) => ( - + ), diff --git a/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx index dfaebe61448..b15a1749339 100644 --- a/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/breaches/BreachIndexView.stories.tsx @@ -8,11 +8,23 @@ import { BreachIndexView, Props as ViewProps } from "./BreachIndexView"; import { getL10n } from "../../../../functions/l10n/storybookAndJest"; import { PublicShell } from "../PublicShell"; import { createRandomHibpListing } from "../../../../../apiMocks/mockData"; +import { defaultExperimentData } from "../../../../../telemetry/generated/nimbus/experiments"; const meta: Meta = { title: "Pages/Public/Breach index", component: (props: ViewProps) => ( - + ), diff --git a/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx index 748e90f3cf5..79bc423ab40 100644 --- a/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/how-it-works/HowItWorksView.stories.tsx @@ -6,11 +6,23 @@ import type { Meta, StoryObj } from "@storybook/react"; import { HowItWorksView } from "./HowItWorksView"; import { getL10n } from "../../../../functions/l10n/storybookAndJest"; import { PublicShell } from "../PublicShell"; +import { defaultExperimentData } from "../../../../../telemetry/generated/nimbus/experiments"; const meta: Meta = { title: "Pages/Public/HowItWorks page", component: () => ( - + {props.children} diff --git a/src/app/(proper_react)/(redesign)/(public)/page.tsx b/src/app/(proper_react)/(redesign)/(public)/page.tsx index 2f3ba51873f..25094a985ab 100644 --- a/src/app/(proper_react)/(redesign)/(public)/page.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/page.tsx @@ -23,7 +23,6 @@ import { getExperiments } from "../../../functions/server/getExperiments"; import { getLocale } from "../../../functions/universal/getLocale"; import { AccountsMetricsFlowProvider } from "../../../../contextProviders/accounts-metrics-flow"; import { getEnabledFeatureFlags } from "../../../../db/tables/featureFlags"; -import { AccountDeletionNotification } from "./AccountDeletionNotification"; type Props = { searchParams: { @@ -74,7 +73,6 @@ export default async function Page({ searchParams }: Props) { service: process.env.OAUTH_CLIENT_ID as string, }} > - {enabledFeatureFlags.includes("LandingPageRedesign") && experimentData["landing-page-redesign"].enabled && experimentData["landing-page-redesign"].variant === "redesign" ? ( From fe4364d314261e4e54c69397a6cd2fb9ab700827 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Tue, 17 Dec 2024 12:40:24 +0100 Subject: [PATCH 06/21] chore: Add stories for landing page redesign --- .../(public)/LandingView.stories.tsx | 2 +- .../(public)/LandingViewRedesign.stories.tsx | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.stories.tsx diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx index c0d8faf66ac..70f99c125bf 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.stories.tsx @@ -11,7 +11,7 @@ import { AccountsMetricsFlowProvider } from "../../../../contextProviders/accoun import { CONST_URL_MONITOR_LANDING_PAGE_ID } from "../../../../constants"; const meta: Meta = { - title: "Pages/Public/Landing page", + title: "Pages/Public/Landing page/Default", component: (props: ViewProps) => { const experimentData = props.experimentData ?? defaultExperimentData; return ( diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.stories.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.stories.tsx new file mode 100644 index 00000000000..821369d111a --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.stories.tsx @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import type { Meta, StoryObj } from "@storybook/react"; +import { View, Props as ViewProps } from "./LandingViewRedesign"; +import { getL10n } from "../../../functions/l10n/storybookAndJest"; +import { PublicShell } from "./PublicShell"; +import { defaultExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; +import { AccountsMetricsFlowProvider } from "../../../../contextProviders/accounts-metrics-flow"; +import { CONST_URL_MONITOR_LANDING_PAGE_ID } from "../../../../constants"; + +const meta: Meta = { + title: "Pages/Public/Landing page/Redesign", + component: (props: ViewProps) => { + const experimentData = props.experimentData ?? defaultExperimentData; + return ( + + + + + + ); + }, + args: { + l10n: getL10n(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const LandingRedesignUs: Story = { + name: "US visitors", + args: { + eligibleForPremium: true, + countryCode: "us", + scanLimitReached: false, + }, +}; + +export const LandingRedesignUsScanLimit: Story = { + name: "US visitors - Scan limit reached", + args: { + eligibleForPremium: true, + countryCode: "us", + scanLimitReached: true, + }, +}; + +export const LandingRedesignNonUs: Story = { + name: "Non-US visitors", + args: { + eligibleForPremium: false, + countryCode: "nz", + }, +}; + +export const LandingRedesignNonUsDe: Story = { + name: "German", + args: { + eligibleForPremium: false, + countryCode: "de", + l10n: getL10n("de"), + }, +}; + +export const LandingRedesignNonUsFr: Story = { + name: "French", + args: { + eligibleForPremium: false, + countryCode: "fr", + l10n: getL10n("fr"), + }, +}; From 56ee8d0f59f023b8dd68a9d6cec1dc0a9a7002f7 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Tue, 17 Dec 2024 13:47:41 +0100 Subject: [PATCH 07/21] chore: Add initial unit tests for landing page redesign --- .../(public)/LandingViewRedesign.test.tsx | 235 ++++++++++++++++++ .../(public)/LandingViewRedesign.tsx | 4 +- .../(public)/PublicShell.stories.ts | 1 + .../(redesign)/(public)/PublicShell.tsx | 34 ++- .../{(public) => }/MobileShell.stories.ts | 14 +- .../(redesign)/MobileShell.test.tsx | 87 +++++++ .../(proper_react)/(redesign)/MobileShell.tsx | 2 + 7 files changed, 353 insertions(+), 24 deletions(-) create mode 100644 src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.test.tsx rename src/app/(proper_react)/(redesign)/{(public) => }/MobileShell.stories.ts (77%) create mode 100644 src/app/(proper_react)/(redesign)/MobileShell.test.tsx diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.test.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.test.tsx new file mode 100644 index 00000000000..781f1d8ae96 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.test.tsx @@ -0,0 +1,235 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { it, expect } from "@jest/globals"; +import { composeStory } from "@storybook/react"; +import { render, screen, within } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; +import { axe } from "jest-axe"; +import { signIn, useSession } from "next-auth/react"; +import { useTelemetry } from "../../../hooks/useTelemetry"; +import Meta, { + LandingRedesignNonUs, + LandingRedesignUs, +} from "./LandingViewRedesign.stories"; +import { deleteAllCookies } from "../../../functions/client/deleteAllCookies"; + +jest.mock("next-auth/react", () => { + return { + signIn: jest.fn(), + useSession: jest.fn(() => { + return {}; + }), + }; +}); +jest.mock("next/navigation", () => ({ + useSearchParams: () => ({ + toString: jest.fn(), + }), + usePathname: jest.fn(), +})); + +jest.mock("../../../hooks/useTelemetry"); + +beforeEach(() => { + // For reasons that are unclear to me, the mock implementation defind in the + // call to `jest.mock` above forgets the implementation. I've spent way too + // long debugging that already, so I'm settling for this :( + const mockedUseSession = useSession as jest.Mock; + mockedUseSession.mockReturnValue({}); + + // Make the rebrand announcement banner show up by default + deleteAllCookies(); +}); + +describe("When Premium is not available", () => { + it("passes the axe accessibility test suite", async () => { + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + const { container } = render(); + expect(await axe(container)).toHaveNoViolations(); + }); + + it("does not show a 'Sign In' button in the header if the user is signed in", () => { + const mockedUseSession = useSession as jest.Mock< + ReturnType, + Parameters + >; + mockedUseSession.mockReturnValue({ + data: { + user: { + email: "arbitrary@example.com", + }, + expires: "2023-06-18T14:48:00.000Z", + }, + status: "authenticated", + update: () => Promise.resolve(null), + }); + + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + render(); + + const signInButton = screen.queryByRole("button", { + name: "Sign In", + }); + + expect(signInButton).not.toBeInTheDocument(); + }); + + it("shows a 'Sign In' button in the header if the user is not signed in", async () => { + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + render(); + + const user = userEvent.setup(); + + const signInButtons = screen.getAllByRole("button", { + name: "Sign In", + }); + await user.click(signInButtons[0]); + await user.click(signInButtons[1]); + expect(signIn).toHaveBeenCalledTimes(2); + }); + + it("counts the number of clicks on the sign-in button at the top", async () => { + const mockedRecord = useTelemetry(); + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + render(); + + const user = userEvent.setup(); + + const signInButtons = screen.getAllByRole("button", { + name: "Sign In", + }); + await user.click(signInButtons[0]); + await user.click(signInButtons[1]); + expect(signIn).toHaveBeenCalledTimes(2); + + expect(mockedRecord).toHaveBeenCalledWith( + "ctaButton", + "click", + expect.objectContaining({ + button_id: "sign_in", + }), + ); + }); +}); + +describe("When Premium is available", () => { + it("passes the axe accessibility test suite", async () => { + const ComposedLanding = composeStory(LandingRedesignUs, Meta); + const { container } = render(); + expect(await axe(container)).toHaveNoViolations(); + }); + + it.each([ + { + name: "How it works", + id: "navbar_how_it_works", + }, + { + name: "Pricing", + id: "navbar_pricing", + }, + { + name: "FAQs", + id: "navbar_faqs", + }, + { + name: "Recent data breaches", + id: "navbar_recent_breaches", + }, + ])("counts the number of clicks %s link in top navbar", async (link) => { + const mockedRecord = useTelemetry(); + const ComposedLanding = composeStory(LandingRedesignUs, Meta); + render(); + + const user = userEvent.setup(); + + const navbarLinks = screen.getAllByRole("link", { name: link.name }); + // jsdom will complain about not being able to navigate to a different page + // after clicking the link; suppress that error, as it's not relevant to the + // test: + jest + .spyOn(console, "error") + .mockImplementationOnce(() => undefined) + .mockImplementationOnce(() => undefined); + await user.click(navbarLinks[0]); + await user.click(navbarLinks[1]); + expect(mockedRecord).toHaveBeenNthCalledWith( + 2, + "link", + "click", + expect.objectContaining({ + link_id: link.id, + }), + ); + }); +}); + +describe("Account deletion confirmation", () => { + it("does not show a confirmaton message if the user has just deleted their account", () => { + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + success: true, + json: jest.fn(() => ({ + flowData: null, + })), + }), + ); + document.cookie = "justDeletedAccount=justDeletedAccount; max-age=0"; + + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + render(); + + const alert = screen.queryByRole("alert"); + + expect(alert).not.toBeInTheDocument(); + }); + + it("shows a confirmaton message if the user has just deleted their account", () => { + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + success: true, + json: jest.fn(() => ({ + flowData: null, + })), + }), + ); + document.cookie = "justDeletedAccount=justDeletedAccount"; + + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + render(); + + const alert = screen.getByRole("alert"); + const confirmationMessage = within(alert).getByText( + "Your ⁨Monitor⁩ account is now deleted.", + ); + + expect(alert).toBeInTheDocument(); + expect(confirmationMessage).toBeInTheDocument(); + }); + + it("hides the 'account deletion' confirmation message when the user dismisses it", async () => { + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + success: true, + json: jest.fn(() => ({ + flowData: null, + })), + }), + ); + const user = userEvent.setup(); + document.cookie = "justDeletedAccount=justDeletedAccount"; + + const ComposedLanding = composeStory(LandingRedesignNonUs, Meta); + render(); + + const alert = screen.getByRole("alert"); + const dismissButton = within(alert).getByRole("button", { + name: "Dismiss", + }); + await user.click(dismissButton); + + expect(alert).not.toBeInTheDocument(); + }); +}); diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx index ad1778814f4..d8db70634cb 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingViewRedesign.tsx @@ -20,9 +20,7 @@ export const View = (props: Props) => { <>
    -
    -

    {props.l10n.getString("landing-all-hero-title")}

    -
    +

    {props.l10n.getString("landing-all-hero-title")}

    ); diff --git a/src/app/(proper_react)/(redesign)/(public)/PublicShell.stories.ts b/src/app/(proper_react)/(redesign)/(public)/PublicShell.stories.ts index 87dd3f7ae6f..76cb9698157 100644 --- a/src/app/(proper_react)/(redesign)/(public)/PublicShell.stories.ts +++ b/src/app/(proper_react)/(redesign)/(public)/PublicShell.stories.ts @@ -18,5 +18,6 @@ export const PublicShellStory: Story = { args: { countryCode: "us", l10n: getL10n("en"), + enabledFeatureFlags: [], }, }; diff --git a/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx b/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx index 2919ab7ca11..1a386522f6a 100644 --- a/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx @@ -62,24 +62,22 @@ export const PublicShell = (props: Props) => { theme="colored" autoClose={false} /> -
    - -
    +
    {props.children}