зеркало из https://github.com/mozilla/fxa.git
Merge pull request #17319 from mozilla/FXA-10145
feat(settings): Create ProductPromo, display for users without Monitor
This commit is contained in:
Коммит
9a52f88a4d
|
@ -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;
|
Загрузка…
Ссылка в новой задаче