diff --git a/libs/shared/assets/src/images/pocket-text-logo.svg b/libs/shared/assets/src/images/pocket-text-logo.svg new file mode 100644 index 00000000000..2a0127844d2 --- /dev/null +++ b/libs/shared/assets/src/images/pocket-text-logo.svg @@ -0,0 +1 @@ + diff --git a/libs/shared/l10n/src/lib/branding.ftl b/libs/shared/l10n/src/lib/branding.ftl index d542c7ac9c3..773db844295 100644 --- a/libs/shared/l10n/src/lib/branding.ftl +++ b/libs/shared/l10n/src/lib/branding.ftl @@ -48,6 +48,7 @@ -product-mozilla-monitor-short = Monitor -product-firefox-relay = Firefox Relay -product-firefox-relay-short = Relay +-product-pocket = Pocket -brand-apple = Apple -brand-apple-pay = Apple Pay diff --git a/packages/fxa-settings/src/components/CardHeader/index.stories.tsx b/packages/fxa-settings/src/components/CardHeader/index.stories.tsx index d82349d08eb..d0e24b66179 100644 --- a/packages/fxa-settings/src/components/CardHeader/index.stories.tsx +++ b/packages/fxa-settings/src/components/CardHeader/index.stories.tsx @@ -70,8 +70,7 @@ export const WithSeparateSubheadingDefaultServiceName = storyWithProps( headingText: MOCK_HEADING, headingTextFtlId: MOCK_DEFAULT_HEADING_FTL_ID, subheadingWithDefaultServiceFtlId: MOCK_DEFAULT_HEADING_FTL_ID, - subheadingWithCustomServiceFtlId: MOCK_CUSTOM_HEADING_FTL_ID, - serviceName: MozServices.Default, + subheadingWithLogoFtlId: MOCK_DEFAULT_HEADING_FTL_ID, }, 'Separate l10n for subheading, with default service' ); @@ -81,8 +80,19 @@ export const WithSeparateSubheadingCustomServiceName = storyWithProps( serviceName: MOCK_SERVICE_NAME, headingText: MOCK_HEADING, headingTextFtlId: MOCK_DEFAULT_HEADING_FTL_ID, - subheadingWithDefaultServiceFtlId: MOCK_DEFAULT_HEADING_FTL_ID, subheadingWithCustomServiceFtlId: MOCK_CUSTOM_HEADING_FTL_ID, + subheadingWithLogoFtlId: MOCK_CUSTOM_HEADING_FTL_ID, }, 'Separate l10n for subheading, with custom service name' ); + +export const WithSeparateSubheadingLogo = storyWithProps( + { + serviceName: MozServices.Pocket, + headingText: MOCK_HEADING, + headingTextFtlId: MOCK_DEFAULT_HEADING_FTL_ID, + subheadingWithCustomServiceFtlId: MOCK_CUSTOM_HEADING_FTL_ID, + subheadingWithLogoFtlId: MOCK_DEFAULT_HEADING_FTL_ID, + }, + 'Separate l10n for subheading, with logo' +); diff --git a/packages/fxa-settings/src/components/CardHeader/index.tsx b/packages/fxa-settings/src/components/CardHeader/index.tsx index 493f5c4206c..a15e18da3ce 100644 --- a/packages/fxa-settings/src/components/CardHeader/index.tsx +++ b/packages/fxa-settings/src/components/CardHeader/index.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { ReactElement } from 'react'; import { FtlMsg } from 'fxa-react/lib/utils'; import { MozServices } from '../../lib/types'; +import PocketTextLogo from '@fxa/shared/assets/images/pocket-text-logo.svg'; // NOTE: this component is heavily tested in components that use it and has complete line // coverage. However, we may file an issue out of FXA-6589 to add more explicit coverage. @@ -39,6 +40,7 @@ interface CardHeaderSeparateSubheadingProps extends CardHeaderRequiredProps { headingTextFtlId: string; subheadingWithDefaultServiceFtlId: string; subheadingWithCustomServiceFtlId: string; + subheadingWithLogoFtlId?: string; serviceName: MozServices; } @@ -107,7 +109,9 @@ function isDefaultService( ); } -function isCmsHeader(props: CardHeaderProps): props is CardHeaderCmsProps { +function isCmsHeader( + props: CardHeaderProps +): props is CardHeaderCmsProps { return ( (props as CardHeaderCmsProps).cmsLogoUrl !== undefined || (props as CardHeaderCmsProps).cmsLogoAltText !== undefined || @@ -127,6 +131,33 @@ function isBasicWithCustomSubheading( ); } +const serviceLogos: { + [key in MozServices]?: ReactElement; +} = { + // This is not inlined because text inside of an SVG can have rendering problems + [MozServices.Pocket]: ( + {MozServices.Pocket} + ), +}; + +// TODO in FXA-8290: do we want to check against these unique client IDs instead +// of serviceName? We have a service names enum, but in theory an RP could change their +// service name and we'd have to update the enum, vs these that don't change. +// export const POCKET_CLIENTIDS = [ +// '7377719276ad44ee', // pocket-mobile +// '749818d3f2e7857f', // pocket-web +// ]; +// This also applies to Monitor +// export const MONITOR_CLIENTIDS = [ +// '802d56ef2a9af9fa', // Mozilla Monitor +// '946bfd23df91404c', // Mozilla Monitor stage +// 'edd29a80019d61a1', // Mozilla Monitor local dev +// }; + const CardHeader = (props: CardHeaderProps) => { const { headingText } = props; @@ -142,7 +173,9 @@ const CardHeader = (props: CardHeaderProps) => { /> )}

{cmsHeadline}

-

{cmsDescription}

+

+ {cmsDescription} +

); } @@ -188,13 +221,25 @@ const CardHeader = (props: CardHeaderProps) => { if (isSeparateSubheading(props)) { const { serviceName = MozServices.Default } = props; const isDefaultService = isDefaultServiceName(serviceName); + const logo = serviceLogos[serviceName]; + const logoElem = {logo}; const subheadingFtlMsgProps = { - id: isDefaultService - ? props.subheadingWithDefaultServiceFtlId - : props.subheadingWithCustomServiceFtlId, - // include `vars={{ serviceName }}` if non-default - ...(!isDefaultService && { vars: { serviceName } }), + // If a logo corresponds to the service name and a logo FTL ID is provided, use that FTL ID. + // Otherwise, if the service is the default service, use the default service FTL ID. + // If non-default, use the custom service FTL ID. + id: + logo && props.subheadingWithLogoFtlId + ? props.subheadingWithLogoFtlId + : isDefaultService + ? props.subheadingWithDefaultServiceFtlId + : props.subheadingWithCustomServiceFtlId, + // include `vars={{ serviceName }}` if non-default and no logo + ...(!isDefaultService && !logo && { vars: { serviceName } }), + // include `elems={{ span: logo }}` if serviceName is given a logo in serviceLogos + ...(logo && { + elems: { span: logo }, + }), }; return ( @@ -203,7 +248,9 @@ const CardHeader = (props: CardHeaderProps) => { {headingText} -

Continue to {serviceName}

+

+ Continue to {logo ? logoElem : serviceName} +

); diff --git a/packages/fxa-settings/src/components/Settings/BentoMenu/en.ftl b/packages/fxa-settings/src/components/Settings/BentoMenu/en.ftl index 325a4b0014f..56ebac25ce2 100644 --- a/packages/fxa-settings/src/components/Settings/BentoMenu/en.ftl +++ b/packages/fxa-settings/src/components/Settings/BentoMenu/en.ftl @@ -5,6 +5,7 @@ bento-menu-tagline = More products from { -brand-mozilla } that protect your pri bento-menu-vpn-2 = { -product-mozilla-vpn } bento-menu-monitor-3 = { -product-mozilla-monitor } +bento-menu-pocket-2 = { -product-pocket } bento-menu-firefox-relay-2 = { -product-firefox-relay } bento-menu-firefox-desktop = { -brand-firefox } Browser for Desktop bento-menu-firefox-mobile = { -brand-firefox } Browser for Mobile diff --git a/packages/fxa-settings/src/components/Settings/BentoMenu/index.test.tsx b/packages/fxa-settings/src/components/Settings/BentoMenu/index.test.tsx index bd4a6412e88..c094ff26805 100644 --- a/packages/fxa-settings/src/components/Settings/BentoMenu/index.test.tsx +++ b/packages/fxa-settings/src/components/Settings/BentoMenu/index.test.tsx @@ -19,6 +19,7 @@ jest.mock('../../../lib/glean', () => ({ bentoFirefoxDesktop: jest.fn(), bentoFirefoxMobile: jest.fn(), bentoMonitor: jest.fn(), + bentoPocket: jest.fn(), bentoRelay: jest.fn(), bentoVpn: jest.fn(), }, @@ -82,6 +83,10 @@ describe('BentoMenu', () => { 'href', 'https://monitor.mozilla.org/?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=bento&utm_content=monitor&utm_campaign=permanent' ); + expect(screen.getByRole('link', { name: /Pocket/ })).toHaveAttribute( + 'href', + 'https://app.adjust.com/hr2n0yz?redirect_macos=https%3A%2F%2Fgetpocket.com%2Fpocket-and-firefox&redirect_windows=https%3A%2F%2Fgetpocket.com%2Fpocket-and-firefox&engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447' + ); expect(screen.getByRole('link', { name: /Firefox Relay/ })).toHaveAttribute( 'href', 'https://relay.firefox.com/?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=bento&utm_content=relay&utm_campaign=permanent' @@ -182,6 +187,19 @@ describe('BentoMenu', () => { }); }); + it('logs metrics event for Pocket link click', async () => { + renderWithLocalizationProvider(); + + userEvent.click(screen.getByRole('button', { name: /Mozilla products/ })); + await waitFor(() => { + expect(screen.getByRole('link', { name: /Pocket/ })).toBeVisible(); + }); + userEvent.click(screen.getByRole('link', { name: /Pocket/ })); + await waitFor(() => { + expect(GleanMetrics.accountPref.bentoPocket).toHaveBeenCalledTimes(1); + }); + }); + it('logs metrics event for Firefox Relay link click', async () => { renderWithLocalizationProvider(); diff --git a/packages/fxa-settings/src/components/Settings/BentoMenu/index.tsx b/packages/fxa-settings/src/components/Settings/BentoMenu/index.tsx index f8ece92450f..3c06857746b 100644 --- a/packages/fxa-settings/src/components/Settings/BentoMenu/index.tsx +++ b/packages/fxa-settings/src/components/Settings/BentoMenu/index.tsx @@ -9,6 +9,7 @@ import LinkExternal from 'fxa-react/components/LinkExternal'; import { useEscKeydownEffect } from '../../../lib/hooks'; import monitorIcon from './monitor.svg'; +import pocketIcon from '@fxa/shared/assets/images/pocket.svg'; import desktopIcon from './desktop.svg'; import mobileIcon from './mobile.svg'; import relayIcon from './relay.svg'; @@ -219,6 +220,19 @@ export const BentoMenu = () => { Mozilla VPN +
  • + GleanMetrics.accountPref.bentoPocket()} + > +
    + +
    + Pocket +
    +
  • { expect(groupedByName['Mozilla Monitor'].length).toEqual(2); }); + it('should show the pocket icon and link', async () => { + await getIconAndServiceLink('Pocket', 'pocket-icon').then((result) => { + expect(result.icon).toBeTruthy(); + expect(result.link).toHaveAttribute('href', 'https://getpocket.com/'); + }); + }); + it('should show the monitor icon and link', async () => { await getIconAndServiceLink('Mozilla Monitor', 'monitor-icon').then( (result) => { diff --git a/packages/fxa-settings/src/components/Settings/PageDeleteAccount/en.ftl b/packages/fxa-settings/src/components/Settings/PageDeleteAccount/en.ftl index b203d4e608a..280867b68ff 100644 --- a/packages/fxa-settings/src/components/Settings/PageDeleteAccount/en.ftl +++ b/packages/fxa-settings/src/components/Settings/PageDeleteAccount/en.ftl @@ -12,6 +12,7 @@ delete-account-product-mozilla-account = { -product-mozilla-account } delete-account-product-mozilla-vpn = { -product-mozilla-vpn } delete-account-product-mdn-plus = { -product-mdn-plus } delete-account-product-mozilla-hubs = { -product-mozilla-hubs } +delete-account-product-pocket = { -product-pocket } delete-account-product-mozilla-monitor = { -product-mozilla-monitor } delete-account-product-firefox-relay = { -product-firefox-relay } delete-account-product-firefox-sync = Syncing { -brand-firefox } data @@ -19,8 +20,8 @@ delete-account-product-firefox-addons = { -brand-firefox } Add-ons delete-account-acknowledge = Please acknowledge that by deleting your account: -delete-account-chk-box-1-v4 = - .label = Any paid subscriptions you have will be canceled +delete-account-chk-box-1-v3 = + .label = Any paid subscriptions you have will be canceled (Except { -product-pocket }) delete-account-chk-box-2 = .label = You may lose saved information and features within { -brand-mozilla } products delete-account-chk-box-3 = @@ -33,6 +34,8 @@ delete-account-continue-button = Continue delete-account-password-input = .label = Enter password +pocket-delete-notice = If you subscribe to Pocket Premium, please make sure that you cancel your subscription before deleting your account. +pocket-delete-notice-marketing = To stop receiving marketing emails from Mozilla Corporation and Mozilla Foundation, you must request deletion of your marketing data. delete-account-cancel-button = Cancel delete-account-delete-button-2 = Delete diff --git a/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.test.tsx b/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.test.tsx index e4226b82fee..6f5443fab44 100644 --- a/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.test.tsx +++ b/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.test.tsx @@ -100,6 +100,7 @@ describe('PageDeleteAccount', () => { "Mozilla Monitor", "MDN Plus", "Mozilla Hubs", + "Pocket", ] `); }); diff --git a/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.tsx b/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.tsx index ba20bc808c4..f3d02ba3ce1 100644 --- a/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.tsx +++ b/packages/fxa-settings/src/components/Settings/PageDeleteAccount/index.tsx @@ -16,6 +16,7 @@ import { Checkbox } from '../Checkbox'; import { useLocalization } from '@fluent/react'; import { Localized } from '@fluent/react'; import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors'; +import LinkExternal from 'fxa-react/components/LinkExternal'; import { getLocalizedErrorMessage } from '../../../lib/error-utils'; import GleanMetrics from '../../../lib/glean'; import { useFtlMsgResolver } from '../../../models/hooks'; @@ -25,8 +26,8 @@ type FormData = { }; const checkboxLabels: Record = { - 'delete-account-chk-box-1-v4': - 'Any paid subscriptions you have will be canceled', + 'delete-account-chk-box-1-v3': + 'Any paid subscriptions you have will be canceled (Except Pocket)', 'delete-account-chk-box-2': 'You may lose saved information and features within Mozilla products', 'delete-account-chk-box-3': @@ -71,6 +72,11 @@ const deleteProducts = [ productName: 'Mozilla Hubs', href: LINK.HUBS, }, + { + localizationId: 'delete-account-product-pocket', + productName: 'Pocket', + href: LINK.POCKET, + }, ]; export const PageDeleteAccount = (_: RouteComponentProps) => { @@ -211,6 +217,48 @@ export const PageDeleteAccount = (_: RouteComponentProps) => { ))} + + cancel your subscription + + ), + }} + > +

    + If you subscribe to Pocket Premium, please make sure that you{' '} + cancel your subscription{' '} + before deleting your account. +

    + + + request deletion of your marketing data. + + ), + }} + > +

    + To stop receiving marketing emails from Mozilla Corporation and + Mozilla Foundation, you must{' '} + + request deletion of your marketing data. + {' '} +

    +

    Please acknowledge that by deleting your account: diff --git a/packages/fxa-settings/src/components/TermsPrivacyAgreement/en.ftl b/packages/fxa-settings/src/components/TermsPrivacyAgreement/en.ftl index e803c45eac5..388a165b7d5 100644 --- a/packages/fxa-settings/src/components/TermsPrivacyAgreement/en.ftl +++ b/packages/fxa-settings/src/components/TermsPrivacyAgreement/en.ftl @@ -3,6 +3,8 @@ # This message is followed by a bulleted list terms-privacy-agreement-intro-2 = By proceeding, you agree to the: +# links to Pocket's Terms of Service and Privacy Notice, part of a bulleted list +terms-privacy-agreement-pocket-2 = { -product-pocket } Terms of Service and Privacy Notice # link to Monitor's Terms of Service and Privacy Notice, part of a bulleted list terms-privacy-agreement-monitor-3 = { -brand-mozilla } Subscription Services Terms of Service and Privacy Notice # links to Mozilla Accounts Terms of Service and Privacy Notice, part of a bulleted list diff --git a/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.stories.tsx b/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.stories.tsx index a713e16d41f..656df9c8ad3 100644 --- a/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.stories.tsx +++ b/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.stories.tsx @@ -20,6 +20,12 @@ export const FirefoxOnly = () => ( ); +export const PocketClient = () => ( + + + +); + export const MonitorClient = () => ( diff --git a/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.test.tsx b/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.test.tsx index 1f44be67c0f..3c5eecba7b1 100644 --- a/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.test.tsx +++ b/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.test.tsx @@ -45,6 +45,25 @@ describe('TermsPrivacyAgreement', () => { expect(linkElements[0]).toHaveAttribute('href', '/legal/terms'); expect(linkElements[1]).toHaveAttribute('href', '/legal/privacy'); }); + + it('renders component as expected for Pocket clients', () => { + renderWithLocalizationProvider(); + // testAllL10n(screen, bundle); + + const linkElements: HTMLElement[] = screen.getAllByRole('link'); + + expect(linkElements).toHaveLength(4); + expect(linkElements[0]).toHaveAttribute( + 'href', + 'https://getpocket.com/tos/' + ); + expect(linkElements[1]).toHaveAttribute( + 'href', + 'https://getpocket.com/privacy/' + ); + expect(linkElements[2]).toHaveAttribute('href', '/legal/terms'); + expect(linkElements[3]).toHaveAttribute('href', '/legal/privacy'); + }); }); it('renders component as expected for Monitor clients', () => { diff --git a/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.tsx b/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.tsx index dac4fbe35fd..5d277f623f0 100644 --- a/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.tsx +++ b/packages/fxa-settings/src/components/TermsPrivacyAgreement/index.tsx @@ -8,12 +8,14 @@ import { Link } from '@reach/router'; import LinkExternal from 'fxa-react/components/LinkExternal'; export type TermsPrivacyAgreementProps = { + isPocketClient?: boolean; isMonitorClient?: boolean; isRelayClient?: boolean; // Relay is oauth RP isFirefoxClientServiceRelay?: boolean; // `service=relay` on Fx desktop or mobile client ID }; const TermsPrivacyAgreement = ({ + isPocketClient = false, isMonitorClient = false, isRelayClient = false, isFirefoxClientServiceRelay = false, @@ -22,12 +24,55 @@ const TermsPrivacyAgreement = ({

    - {isMonitorClient || isFirefoxClientServiceRelay || isRelayClient ? ( + {isPocketClient || + isMonitorClient || + isFirefoxClientServiceRelay || + isRelayClient ? ( <>

    By proceeding, you agree to the:

      + {isPocketClient && ( + + Terms of Service + + ), + pocketPrivacy: ( + + Privacy Notice + + ), + }} + > +
    • + Pocket{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Notice + +
    • +
      + )} {(isMonitorClient || isFirefoxClientServiceRelay || isRelayClient) && ( diff --git a/packages/fxa-settings/src/constants/index.tsx b/packages/fxa-settings/src/constants/index.tsx index b89b88800ee..da733dde511 100644 --- a/packages/fxa-settings/src/constants/index.tsx +++ b/packages/fxa-settings/src/constants/index.tsx @@ -31,6 +31,9 @@ export const LINK = { MDN: 'https://developer.mozilla.org/', MONITOR: 'https://monitor.mozilla.org/', MONITOR_STAGE: 'https://monitor-stage.allizom.org/', + MONITOR_PLUS: 'https://monitor.mozilla.org/subscription-plans', + MONITOR_PLUS_STAGE: 'https://monitor-stage.allizom.org/subscription-plans', + POCKET: 'https://getpocket.com/', RELAY: 'https://relay.firefox.com/', VPN: 'https://vpn.mozilla.org/', }; diff --git a/packages/fxa-settings/src/lib/constants.ts b/packages/fxa-settings/src/lib/constants.ts index 8e17a5d66eb..252893b97d3 100644 --- a/packages/fxa-settings/src/lib/constants.ts +++ b/packages/fxa-settings/src/lib/constants.ts @@ -134,6 +134,9 @@ export const Constants = { MOZ_ORG_SYNC_GET_STARTED_LINK: 'https://www.mozilla.org/firefox/sync?utm_source=fx-website&utm_medium=fx-accounts&utm_campaign=fx-signup&utm_content=fx-sync-get-started', //eslint-disable-line max-len + POCKET_MORE_INFO_LINK: + 'https://support.mozilla.org/kb/pocket-firefox-account-migration', + // 20 most popular email domains, used for metrics. Matches the list // we use in the auth server, converted to a map for faster lookup. POPULAR_EMAIL_DOMAINS: popularDomains.reduce( diff --git a/packages/fxa-settings/src/lib/glean/index.test.ts b/packages/fxa-settings/src/lib/glean/index.test.ts index 87c8fea8a2c..7608a1af7a0 100644 --- a/packages/fxa-settings/src/lib/glean/index.test.ts +++ b/packages/fxa-settings/src/lib/glean/index.test.ts @@ -909,6 +909,15 @@ describe('lib/glean', () => { sinon.assert.calledOnce(spy); }); + it('submits a ping with the account_pref_bento_pocket event name', async () => { + GleanMetrics.accountPref.bentoPocket(); + const spy = sandbox.spy(accountPref.bentoPocket, 'record'); + await GleanMetrics.isDone(); + sinon.assert.calledOnce(setEventNameStub); + sinon.assert.calledWith(setEventNameStub, 'account_pref_bento_pocket'); + sinon.assert.calledOnce(spy); + }); + it('submits a ping with the account_pref_bento_relay event name', async () => { GleanMetrics.accountPref.bentoRelay(); const spy = sandbox.spy(accountPref.bentoRelay, 'record'); diff --git a/packages/fxa-settings/src/lib/glean/index.ts b/packages/fxa-settings/src/lib/glean/index.ts index f91b3f0f5b2..79d74c295f2 100644 --- a/packages/fxa-settings/src/lib/glean/index.ts +++ b/packages/fxa-settings/src/lib/glean/index.ts @@ -33,10 +33,7 @@ import * as deleteAccount from 'fxa-shared/metrics/glean/web/deleteAccount'; import * as thirdPartyAuth from 'fxa-shared/metrics/glean/web/thirdPartyAuth'; import * as thirdPartyAuthSetPassword from 'fxa-shared/metrics/glean/web/thirdPartyAuthSetPassword'; import { userIdSha256, userId } from 'fxa-shared/metrics/glean/web/account'; -import { - appFramework, - cmsCustomizationEnrollment, -} from 'fxa-shared/metrics/glean/web/event'; +import { appFramework, cmsCustomizationEnrollment } from 'fxa-shared/metrics/glean/web/event'; import { oauthClientId, service, @@ -566,6 +563,9 @@ const recordEventMetric = ( case 'account_pref_bento_monitor': accountPref.bentoMonitor.record(); break; + case 'account_pref_bento_pocket': + accountPref.bentoPocket.record(); + break; case 'account_pref_bento_relay': accountPref.bentoRelay.record(); break; diff --git a/packages/fxa-settings/src/lib/types.ts b/packages/fxa-settings/src/lib/types.ts index 73b8b7585a2..b9e015734c4 100644 --- a/packages/fxa-settings/src/lib/types.ts +++ b/packages/fxa-settings/src/lib/types.ts @@ -55,6 +55,7 @@ export enum MozServices { Monitor = 'Mozilla Monitor', FirefoxSync = 'Firefox Sync', MozillaVPN = 'Mozilla VPN', + Pocket = 'Pocket', Relay = 'Mozilla Relay', TestService = '123Done', MonitorStage = 'Mozilla Monitor Stage', diff --git a/packages/fxa-settings/src/models/integrations/client-matching.ts b/packages/fxa-settings/src/models/integrations/client-matching.ts index beab21428fe..ea40f1780b4 100644 --- a/packages/fxa-settings/src/models/integrations/client-matching.ts +++ b/packages/fxa-settings/src/models/integrations/client-matching.ts @@ -9,12 +9,21 @@ export const MONITOR_CLIENTIDS = [ 'edd29a80019d61a1', // Mozilla Monitor local dev ]; +export const POCKET_CLIENTIDS = [ + '7377719276ad44ee', // pocket-mobile + '749818d3f2e7857f', // pocket-web +]; + export const RELAY_CLIENTIDS = [ '41b4363ae36440a9', // Relay stage '723aa3bce05884d8', // Relay dev '9ebfe2c2f9ea3c58', // Relay prod ]; +export const isClientPocket = (clientId?: string) => { + return !!(clientId && POCKET_CLIENTIDS.includes(clientId)); +}; + export const isClientMonitor = (clientId?: string) => { return !!(clientId && MONITOR_CLIENTIDS.includes(clientId)); }; diff --git a/packages/fxa-settings/src/models/integrations/integration.ts b/packages/fxa-settings/src/models/integrations/integration.ts index 90f16cf2693..28762f273c7 100644 --- a/packages/fxa-settings/src/models/integrations/integration.ts +++ b/packages/fxa-settings/src/models/integrations/integration.ts @@ -130,6 +130,8 @@ export class GenericIntegration< case MozServices.MozillaVPN: return MozServices.MozillaVPN; + case MozServices.Pocket: + return MozServices.Pocket; case MozServices.TestService: return MozServices.TestService; diff --git a/packages/fxa-settings/src/pages/Index/en.ftl b/packages/fxa-settings/src/pages/Index/en.ftl index 0c125c757a9..ee826325d5d 100644 --- a/packages/fxa-settings/src/pages/Index/en.ftl +++ b/packages/fxa-settings/src/pages/Index/en.ftl @@ -7,6 +7,7 @@ index-relay-header = Create an email mask index-relay-subheader = Please provide the email address where you’d like to forward emails from your masked email. # $serviceName - the service (e.g., Pontoon) that the user is signing into with a Mozilla account index-subheader-with-servicename = Continue to { $serviceName } +index-subheader-with-logo = Continue to { $serviceLogo } index-subheader-default = Continue to account settings index-cta = Sign up or sign in index-account-info = A { -product-mozilla-account } also unlocks access to more privacy-protecting products from { -brand-mozilla }. diff --git a/packages/fxa-settings/src/pages/Index/index.stories.tsx b/packages/fxa-settings/src/pages/Index/index.stories.tsx index bf6da5b5d79..eb760a9be6e 100644 --- a/packages/fxa-settings/src/pages/Index/index.stories.tsx +++ b/packages/fxa-settings/src/pages/Index/index.stories.tsx @@ -14,6 +14,7 @@ import { } from './mocks'; import { MONITOR_CLIENTIDS, + POCKET_CLIENTIDS, } from '../../models/integrations/client-matching'; import { MozServices } from '../../lib/types'; import { MOCK_EMAIL, MOCK_CMS_INFO } from '../mocks'; @@ -60,6 +61,13 @@ export const Monitor = storyWithProps({ serviceName: MozServices.Monitor, }); +export const Pocket = storyWithProps({ + integration: createMockIndexOAuthIntegration({ + clientId: POCKET_CLIENTIDS[0], + }), + serviceName: MozServices.Pocket, +}); + export const WithCms = storyWithProps({ integration: createMockIndexOAuthNativeIntegration({ isFirefoxClientServiceRelay: true, diff --git a/packages/fxa-settings/src/pages/Index/index.test.tsx b/packages/fxa-settings/src/pages/Index/index.test.tsx index a622f98af78..7dfadd97148 100644 --- a/packages/fxa-settings/src/pages/Index/index.test.tsx +++ b/packages/fxa-settings/src/pages/Index/index.test.tsx @@ -11,8 +11,8 @@ import { Subject, } from './mocks'; import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; +import { POCKET_CLIENTIDS } from '../../models/integrations/client-matching'; import { MozServices } from '../../lib/types'; -import { MONITOR_CLIENTIDS } from '../../models/integrations/client-matching'; import GleanMetrics from '../../lib/glean'; import { MOCK_CMS_INFO } from '../mocks'; @@ -221,7 +221,7 @@ describe('Index page', () => { renderWithLocalizationProvider( @@ -231,6 +231,29 @@ describe('Index page', () => { }); }); + it('renders as expected when client is Pocket', () => { + renderWithLocalizationProvider( + + ); + + screen.getByRole('heading', { name: 'Enter your email' }); + screen.getByAltText('Pocket'); + + thirdPartyAuthWithSeparatorRendered(); + + const tosLinks = screen.getAllByRole('link', { + name: /Terms of Service/, + }); + + expect(tosLinks[0]).toHaveAttribute('href', 'https://getpocket.com/tos/'); + expect(tosLinks[1]).toHaveAttribute('href', '/legal/terms'); + }); + describe('glean metrics', () => { it('emits emailFirst.view on initial render', () => { const viewSpy = jest.spyOn(GleanMetrics.emailFirst, 'view'); diff --git a/packages/fxa-settings/src/pages/Index/index.tsx b/packages/fxa-settings/src/pages/Index/index.tsx index 70b435da2c2..fc9716340c3 100644 --- a/packages/fxa-settings/src/pages/Index/index.tsx +++ b/packages/fxa-settings/src/pages/Index/index.tsx @@ -13,6 +13,7 @@ import ThirdPartyAuth from '../../components/ThirdPartyAuth'; import TermsPrivacyAgreement from '../../components/TermsPrivacyAgreement'; import { isClientMonitor, + isClientPocket, isClientRelay, } from '../../models/integrations/client-matching'; import { isOAuthIntegration } from '../../models'; @@ -40,6 +41,7 @@ export const Index = ({ const isSync = integration.isSync(); const isFirefoxClientServiceRelay = integration.isFirefoxClientServiceRelay(); const isOAuth = isOAuthIntegration(integration); + const isPocketClient = isOAuth && isClientPocket(clientId); const isMonitorClient = isOAuth && isClientMonitor(clientId); const isRelayClient = isOAuth && isClientRelay(clientId); const [isSubmitting, setIsSubmitting] = useState(false); @@ -149,6 +151,7 @@ export const Index = ({ headingTextFtlId="index-header" subheadingWithDefaultServiceFtlId="index-subheader-default" subheadingWithCustomServiceFtlId="index-subheader-with-servicename" + subheadingWithLogoFtlId="index-subheader-with-logo" {...{ clientId, serviceName }} /> )} @@ -212,6 +215,7 @@ export const Index = ({ )} elements appear as a subheading. signin-password-needed-header-2 = Enter your password for your { -product-mozilla-account } +# $serviceLogo - an image of the logo of the service which the user is authenticating for. +# For languages structured like English, the phrase can read "to continue to" +signin-subheader-with-logo = Continue to { $serviceLogo } + # $serviceName - the name of the service which the user authenticating for # For languages structured like English, the phrase can read "to continue to { $serviceName }" signin-subheader-without-logo-with-servicename = Continue to { $serviceName } diff --git a/packages/fxa-settings/src/pages/Signin/index.stories.tsx b/packages/fxa-settings/src/pages/Signin/index.stories.tsx index 1bc5241a464..d0f31013ece 100644 --- a/packages/fxa-settings/src/pages/Signin/index.stories.tsx +++ b/packages/fxa-settings/src/pages/Signin/index.stories.tsx @@ -4,6 +4,7 @@ import React from 'react'; import Signin from '.'; +import { MozServices } from '../../lib/types'; import { Meta } from '@storybook/react'; import { Subject, @@ -16,7 +17,6 @@ import { SigninProps } from './interfaces'; import { MOCK_SERVICE, MOCK_SESSION_TOKEN } from '../mocks'; import { AuthUiErrors } from '../../lib/auth-errors/auth-errors'; import { BeginSigninError } from '../../lib/error-utils'; -import { MozServices } from '../../lib/types'; export default { title: 'Pages/Signin', @@ -44,6 +44,11 @@ export const SignInToRelyingPartyWithPassword = storyWithProps({ serviceName: MOCK_SERVICE, }); +export const SignInToPocketWithPassword = storyWithProps({ + serviceName: MozServices.Pocket, + integration: createMockSigninOAuthIntegration(), +}); + export const SignInToSettingsWithCachedCredentials = storyWithProps({ sessionToken: MOCK_SESSION_TOKEN, }); @@ -52,6 +57,12 @@ export const SignInToRelyingPartyWithCachedCredentials = storyWithProps({ serviceName: MOCK_SERVICE, }); +export const SignInToPocketWithCachedCredentials = storyWithProps({ + sessionToken: MOCK_SESSION_TOKEN, + serviceName: MozServices.Pocket, + integration: createMockSigninOAuthIntegration({ wantsKeys: false }), +}); + export const SignInToSyncWithCachedCredentials = storyWithProps({ sessionToken: MOCK_SESSION_TOKEN, integration: createMockSigninOAuthIntegration({ wantsKeys: true }), diff --git a/packages/fxa-settings/src/pages/Signin/index.test.tsx b/packages/fxa-settings/src/pages/Signin/index.test.tsx index a16cbe1a469..26d64b25d84 100644 --- a/packages/fxa-settings/src/pages/Signin/index.test.tsx +++ b/packages/fxa-settings/src/pages/Signin/index.test.tsx @@ -40,7 +40,10 @@ import VerificationMethods from '../../constants/verification-methods'; import VerificationReasons from '../../constants/verification-reasons'; import { SigninProps } from './interfaces'; import { AuthUiErrors } from '../../lib/auth-errors/auth-errors'; -import { MONITOR_CLIENTIDS } from '../../models/integrations/client-matching'; +import { + MONITOR_CLIENTIDS, + POCKET_CLIENTIDS, +} from '../../models/integrations/client-matching'; import firefox from '../../lib/channels/firefox'; import { navigate } from '@reach/router'; import { @@ -1326,6 +1329,53 @@ describe('Signin component', () => { }); }); + describe('when client is Pocket', () => { + it('shows Pocket in header', () => { + renderWithLocalizationProvider( + + ); + + const pocketLogo = screen.getByAltText('Pocket'); + expect(pocketLogo).toBeInTheDocument(); + }); + + it('shows Pocket-specific TOS', () => { + renderWithLocalizationProvider( + + ); + + // Pocket links should always open in a new window (announced by screen readers) + const pocketTermsLink = screen.getByRole('link', { + name: 'Terms of Service Opens in new window', + }); + const pocketPrivacyLink = screen.getByRole('link', { + name: 'Privacy Notice Opens in new window', + }); + + expect(pocketTermsLink).toHaveAttribute( + 'href', + 'https://getpocket.com/tos/' + ); + expect(pocketPrivacyLink).toHaveAttribute( + 'href', + 'https://getpocket.com/privacy/' + ); + }); + }); + describe('when client is Monitor', () => { it('shows Monitor-specific TOS', async () => { renderWithLocalizationProvider( diff --git a/packages/fxa-settings/src/pages/Signin/index.tsx b/packages/fxa-settings/src/pages/Signin/index.tsx index 07370111ac1..e07b2a86272 100644 --- a/packages/fxa-settings/src/pages/Signin/index.tsx +++ b/packages/fxa-settings/src/pages/Signin/index.tsx @@ -28,6 +28,7 @@ import { } from '../../models'; import { isClientMonitor, + isClientPocket, isClientRelay, } from '../../models/integrations/client-matching'; import { SigninFormData, SigninProps } from './interfaces'; @@ -87,6 +88,7 @@ const Signin = ({ const isOAuth = isOAuthIntegration(integration); const isFirefoxClientServiceRelay = integration.isFirefoxClientServiceRelay(); const clientId = integration.getClientId(); + const isPocketClient = isOAuth && isClientPocket(clientId); const isMonitorClient = isOAuth && isClientMonitor(clientId); const isRelayClient = isOAuth && isClientRelay(clientId); const hasLinkedAccountAndNoPassword = hasLinkedAccount && !hasPassword; @@ -402,6 +404,7 @@ const Signin = ({ headingTextFtlId="signin-header" subheadingWithDefaultServiceFtlId="signin-subheader-without-logo-default" subheadingWithCustomServiceFtlId="signin-subheader-without-logo-with-servicename" + subheadingWithLogoFtlId="signin-subheader-with-logo" {...{ clientId, serviceName, @@ -519,6 +522,7 @@ const Signin = ({ ; export const CantChangeEmail = () => ; +export const ClientIsPocket = () => ( + +); export const ClientIsMonitor = () => ( { expect(firefoxPrivacyLink).toHaveAttribute('href', '/legal/privacy'); }); + it('shows an info banner and Pocket-specific TOS when client is Pocket', async () => { + renderWithLocalizationProvider( + + ); + + const infoBannerLink = screen.getByRole('link', { + name: /Find out here/, + }); + await waitFor(() => { + expect(infoBannerLink).toBeInTheDocument(); + }); + + // info banner is dismissible + const infoBannerDismissButton = screen.getByRole('button', { + name: 'Close banner', + }); + fireEvent.click(infoBannerDismissButton); + await waitFor(() => { + expect(infoBannerLink).not.toBeInTheDocument(); + }); + + // Pocket links should always open in a new window (announced by screen readers) + const pocketTermsLink = screen.getByRole('link', { + name: 'Terms of Service Opens in new window', + }); + const pocketPrivacyLink = screen.getByRole('link', { + name: 'Privacy Notice Opens in new window', + }); + + expect(pocketTermsLink).toHaveAttribute( + 'href', + 'https://getpocket.com/tos/' + ); + expect(pocketPrivacyLink).toHaveAttribute( + 'href', + 'https://getpocket.com/privacy/' + ); + }); + it('renders as expected when integration is sync', async () => { await act(() => { renderWithLocalizationProvider( diff --git a/packages/fxa-settings/src/pages/Signup/index.tsx b/packages/fxa-settings/src/pages/Signup/index.tsx index 78e4a8c4932..0cb375b0b0d 100644 --- a/packages/fxa-settings/src/pages/Signup/index.tsx +++ b/packages/fxa-settings/src/pages/Signup/index.tsx @@ -32,6 +32,7 @@ import { } from '../../models'; import { isClientMonitor, + isClientPocket, isClientRelay, } from '../../models/integrations/client-matching'; import { SignupFormData, SignupProps } from './interfaces'; @@ -80,6 +81,10 @@ export const Signup = ({ const [beginSignupLoading, setBeginSignupLoading] = useState(false); const [bannerErrorText, setBannerErrorText] = useState(''); const [isFocused, setIsFocused] = useState(false); + const [ + isAccountSuggestionBannerVisible, + setIsAccountSuggestionBannerVisible, + ] = useState(false); const navigateWithQuery = useNavigateWithQuery(); // no newsletters are selected by default @@ -91,6 +96,10 @@ export const Signup = ({ useEffect(() => { if (isOAuth) { const clientId = integration.getClientId(); + if (isClientPocket(clientId)) { + setClient(MozServices.Pocket); + setIsAccountSuggestionBannerVisible(true); + } if (isClientMonitor(clientId)) { setClient(MozServices.Monitor); } @@ -332,6 +341,30 @@ export const Signup = ({ )} + {/* AccountSuggestion is only shown to Pocket clients */} + {isAccountSuggestionBannerVisible && ( + setIsAccountSuggestionBannerVisible(false), + }} + /> + )} +

      {email}

      @@ -389,6 +422,7 @@ export const Signup = ({ )}