Merge pull request #17319 from mozilla/FXA-10145

feat(settings): Create ProductPromo, display for users without Monitor
This commit is contained in:
Lauren Zugai 2024-08-05 12:19:56 -05:00 коммит произвёл GitHub
Родитель a8e4d39a3f cd7731e9fa
Коммит 9a52f88a4d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 229 добавлений и 18 удалений

Просмотреть файл

@ -12,18 +12,18 @@ import DropDownAvatarMenu from '../DropDownAvatarMenu';
import { ReactComponent as Help } from './help.svg';
import { ReactComponent as Menu } from './menu.svg';
import { ReactComponent as Close } from './close.svg';
import Nav from '../Nav';
import { SettingsIntegration } from '../interfaces';
import Sidebar from '../Sidebar';
export const HeaderLockup = ({
integration,
}: {
integration: SettingsIntegration;
}) => {
const [navRevealedState, setNavState] = useState(false);
const [sidebarRevealedState, setNavState] = useState(false);
const { l10n } = useLocalization();
const localizedHelpText = l10n.getString('header-help', null, 'Help');
const localizedMenuText = navRevealedState
const localizedMenuText = sidebarRevealedState
? l10n.getString('header-menu-open', null, 'Close menu')
: l10n.getString('header-menu-closed', null, 'Site navigation menu');
@ -35,15 +35,15 @@ export const HeaderLockup = ({
aria-label={localizedMenuText}
title={localizedMenuText}
aria-haspopup={true}
aria-expanded={navRevealedState}
onClick={() => setNavState(!navRevealedState)}
aria-expanded={sidebarRevealedState}
onClick={() => setNavState(!sidebarRevealedState)}
>
{navRevealedState ? (
{sidebarRevealedState ? (
<Close className="text-violet-900 w-8" />
) : (
<Menu className="text-violet-900 w-8" />
)}
{navRevealedState && <Nav />}
{sidebarRevealedState && <Sidebar />}
</button>
<Localized id="header-back-to-top-link" attrs={{ title: true }}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}

Просмотреть файл

@ -9,6 +9,14 @@ import { ReactComponent as OpenExternal } from './open-external.svg';
import { useAccount, useConfig } from '../../../models';
import { Localized } from '@fluent/react';
export interface NavRefProps {
profileRef?: React.MutableRefObject<HTMLDivElement | null>;
securityRef?: React.MutableRefObject<HTMLDivElement | null>;
connectedServicesRef?: React.MutableRefObject<HTMLDivElement | null>;
linkedAccountsRef?: React.MutableRefObject<HTMLDivElement | null>;
dataCollectionRef?: React.MutableRefObject<HTMLDivElement | null>;
}
const navActiveClass = 'nav-active';
// Update the active nav class when this percentage of a section is shown on screen
@ -21,13 +29,7 @@ export const Nav = ({
connectedServicesRef,
linkedAccountsRef,
dataCollectionRef,
}: {
profileRef?: React.MutableRefObject<HTMLDivElement | null>;
securityRef?: React.MutableRefObject<HTMLDivElement | null>;
connectedServicesRef?: React.MutableRefObject<HTMLDivElement | null>;
linkedAccountsRef?: React.MutableRefObject<HTMLDivElement | null>;
dataCollectionRef?: React.MutableRefObject<HTMLDivElement | null>;
}) => {
}: NavRefProps) => {
const account = useAccount();
const config = useConfig();
const profileLinkRef = useRef<HTMLAnchorElement>(null);
@ -134,8 +136,7 @@ export const Nav = ({
return (
<nav
// top-[7.69rem] allows the sticky nav header to align exactly with first section heading
className="font-header fixed bg-white w-full inset-0 mt-19 desktop:mt-0 desktop:sticky desktop:top-[7.69rem] desktop:bg-transparent text-xl desktop:text-base"
className="font-header desktop:w-11/12 text-xl desktop:text-base"
data-testid="nav"
>
<ul className="px-6 py-8 tablet:px-8 desktop:p-0 text-start">

Просмотреть файл

@ -4,7 +4,6 @@
import React, { useEffect, useRef } from 'react';
import { Link, RouteComponentProps } from '@reach/router';
import Nav from '../Nav';
import Security from '../Security';
import { Profile } from '../Profile';
import ConnectedServices from '../ConnectedServices';
@ -16,6 +15,8 @@ import { DeleteAccountPath } from 'fxa-settings/src/constants';
import { Localized } from '@fluent/react';
import DataCollection from '../DataCollection';
import GleanMetrics from '../../../lib/glean';
import ProductPromo, { ProductPromoType } from '../ProductPromo';
import SideBar from '../Sidebar';
export const PageSettings = (_: RouteComponentProps) => {
const { uid } = useAccount();
@ -40,7 +41,7 @@ export const PageSettings = (_: RouteComponentProps) => {
return (
<div id="fxa-settings" className="flex">
<div className="hidden desktop:block desktop:flex-2">
<Nav
<SideBar
{...{
profileRef,
securityRef,
@ -68,6 +69,7 @@ export const PageSettings = (_: RouteComponentProps) => {
</Link>
</Localized>
</div>
<ProductPromo type={ProductPromoType.Settings} />
</div>
</div>
);

Просмотреть файл

@ -0,0 +1,7 @@
## Product promotion
product-promo-monitor =
.alt = { -product-mozilla-monitor }
product-promo-monitor-description = Find where your private info is exposed — and take it back
# Links out to the Monitor site
product-promo-monitor-cta = Get free scan

Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react';
import ProductPromo, { ProductPromoProps, ProductPromoType } from '.';
import { Meta } from '@storybook/react';
import { withLocalization } from 'fxa-react/lib/storybooks';
export default {
title: 'Components/Settings/ProductPromo',
component: ProductPromo,
decorators: [withLocalization],
} as Meta;
const storyWithProps = (props: ProductPromoProps, storyName?: string) => {
const story = () => <ProductPromo {...props} />;
if (storyName) story.storyName = storyName;
return story;
};
export const SettingsWithMonitor = storyWithProps(
{ type: ProductPromoType.Settings },
'Settings with Monitor (resize window)'
);
export const SidebarWithMonitor = storyWithProps(
{ type: ProductPromoType.Sidebar },
'Sidebar with Monitor (resize window)'
);

Просмотреть файл

@ -0,0 +1,55 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react';
import { screen } from '@testing-library/react';
import ProductPromo from '.';
import { Account, AppContext } from '../../../models';
import { MOCK_SERVICES } from '../ConnectedServices/mocks';
import { MozServices } from '../../../lib/types';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
import { mockAppContext } from '../../../models/mocks';
import { MonitorLink } from '../../../constants';
// List all services this component handles
const PRODUCT_PROMO_SERVICES = [MozServices.Monitor];
describe('ProductPromo', () => {
it('renders nothing if user has all products', async () => {
const services = MOCK_SERVICES.filter((service) =>
// TODO: MozServices / string discrepancy, FXA-6802
PRODUCT_PROMO_SERVICES.includes(service.name as MozServices)
);
const account = {
attachedClients: services,
} as unknown as Account;
const { container } = renderWithLocalizationProvider(
<AppContext.Provider value={mockAppContext({ account })}>
<ProductPromo />
</AppContext.Provider>
);
expect(container.firstChild).toBeNull();
});
it('renders Monitor promo if user does not have Monitor', async () => {
const account = {
attachedClients: [],
} as unknown as Account;
renderWithLocalizationProvider(
<AppContext.Provider value={mockAppContext({ account })}>
<ProductPromo />
</AppContext.Provider>
);
screen.getByAltText('Mozilla Monitor');
screen.getByText(
'Find where your private info is exposed — and take it back'
);
expect(screen.getByRole('link', { name: /Get free scan/ })).toHaveAttribute(
'href',
MonitorLink
);
});
});

Просмотреть файл

@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react';
import LinkExternal from 'fxa-react/components/LinkExternal';
import monitorTextLogo from './monitor-text-logo.svg';
import { FtlMsg } from 'fxa-react/lib/utils';
import classNames from 'classnames';
import { MonitorLink } from '../../../constants';
import { MozServices } from '../../../lib/types';
import { useAccount } from '../../../models';
export enum ProductPromoType {
Sidebar = 'sidebar',
Settings = 'settings',
}
export interface ProductPromoProps {
type?: ProductPromoType;
// product?: MozServices;
}
export const ProductPromo = ({
type = ProductPromoType.Sidebar,
}: ProductPromoProps) => {
const { attachedClients } = useAccount();
const hasMonitor = attachedClients.some(
({ name }) => name === MozServices.Monitor
);
// const hasMonitorPlus = subscriptions.some(
// ({ productName }) => productName === MozServices.MonitorPlus
// );
if (hasMonitor) {
return <></>;
}
// if (hasMonitor) {
// Glean view event
// }
// if (hasMonitorPlus) {
// Glean view event
// }
return (
<aside
className={classNames(
'bg-white rounded-lg desktop:w-11/12 desktop:max-w-56 desktop:p-4 desktop:pb-6 text-grey-600 text-lg desktop:text-sm text-start',
type === ProductPromoType.Sidebar &&
'px-6 mt-4 desktop:mt-20 desktop:max-w-80 desktop:w-11/12',
type === ProductPromoType.Settings &&
'desktop:hidden mt-12 px-5 py-3 mb-16'
)}
>
<div
className={classNames(
type === ProductPromoType.Sidebar &&
'border-2 border-grey-100 desktop:border-0 rounded-lg px-5 py-3 desktop:px-0 desktop:py-0'
)}
>
<h2>
<FtlMsg id="product-promo-monitor">
<img
src={monitorTextLogo}
alt="Mozilla Monitor"
className="w-52 desktop:w-40 h-auto"
/>
</FtlMsg>
</h2>
<p className="my-2">
<FtlMsg id="product-promo-monitor-description">
Find where your private info is exposed and take it back
</FtlMsg>
</p>
{/* possible todo, link to their stage env in stage? can do with FXA-10147 */}
<LinkExternal href={MonitorLink} className="link-blue">
<FtlMsg id="product-promo-monitor-cta">Get free scan</FtlMsg>
</LinkExternal>
</div>
</aside>
);
};
export default ProductPromo;

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 7.4 KiB

Просмотреть файл

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react';
import Nav, { NavRefProps } from '../Nav';
import ProductPromo, { ProductPromoType } from '../ProductPromo';
export const SideBar = ({
profileRef,
securityRef,
connectedServicesRef,
linkedAccountsRef,
dataCollectionRef,
}: NavRefProps) => {
// top-[7.69rem] allows the sticky nav header to align exactly with first section heading
return (
<div className="fixed desktop:sticky desktop:top-[7.69rem] inset-0 bg-white desktop:bg-transparent w-full mt-19 desktop:mt-0">
<Nav
{...{
profileRef,
securityRef,
connectedServicesRef,
linkedAccountsRef,
dataCollectionRef,
}}
/>
<ProductPromo type={ProductPromoType.Sidebar} />
</div>
);
};
export default SideBar;