diff --git a/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx b/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx
index 4a0fec7347..c8f1bb84d3 100644
--- a/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx
+++ b/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx
@@ -6,9 +6,19 @@ import React from 'react';
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import PageSettings from '.';
-import { renderWithRouter } from '../../../models/mocks';
+import {
+ MOCK_ACCOUNT,
+ mockAppContext,
+ renderWithRouter,
+} from '../../../models/mocks';
import * as Metrics from '../../../lib/metrics';
import GleanMetrics from '../../../lib/glean';
+import { Account, AppContext } from '../../../models';
+import {
+ ALL_PRODUCT_PROMO_SERVICES,
+ ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
+} from '../../../pages/mocks';
+import { MOCK_SERVICES } from '../ConnectedServices/mocks';
jest.mock('../../../lib/metrics', () => ({
setProperties: jest.fn(),
@@ -38,6 +48,9 @@ beforeEach(() => {
});
describe('PageSettings', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
it('renders without imploding', async () => {
renderWithRouter();
expect(screen.getByTestId('settings-profile')).toBeInTheDocument();
@@ -68,5 +81,42 @@ describe('PageSettings', () => {
);
expect(GleanMetrics.deleteAccount.settingsSubmit).toHaveBeenCalled();
});
+
+ describe('product promo event', () => {
+ it('user does not have Monitor', async () => {
+ const account = {
+ ...MOCK_ACCOUNT,
+ attachedClients: [],
+ subscriptions: [],
+ } as unknown as Account;
+ renderWithRouter(
+
+
+
+ );
+ expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledTimes(1);
+ expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledWith({
+ event: { reason: 'free' },
+ });
+ });
+ it('user has all products and subscriptions', async () => {
+ const attachedClients = MOCK_SERVICES.filter((service) =>
+ ALL_PRODUCT_PROMO_SERVICES.some(
+ (promoService) => promoService.name === service.name
+ )
+ );
+ const account = {
+ ...MOCK_ACCOUNT,
+ attachedClients,
+ subscriptions: ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
+ } as unknown as Account;
+ renderWithRouter(
+
+
+
+ );
+ expect(GleanMetrics.accountPref.promoMonitorView).not.toBeCalled();
+ });
+ });
});
});
diff --git a/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx b/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx
index 5485916292..3d7e72ff0c 100644
--- a/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx
+++ b/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx
@@ -2,7 +2,7 @@
* 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, { useEffect, useRef } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { Link, RouteComponentProps } from '@reach/router';
import Security from '../Security';
import { Profile } from '../Profile';
@@ -15,14 +15,17 @@ import { SETTINGS_PATH } 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 ProductPromo, {
+ getProductPromoData,
+ ProductPromoType,
+} from '../ProductPromo';
import SideBar from '../Sidebar';
import NotificationPromoBanner from '../../NotificationPromoBanner';
import keyImage from '../../NotificationPromoBanner/key.svg';
import Head from 'fxa-react/components/Head';
export const PageSettings = (_: RouteComponentProps) => {
- const { uid, recoveryKey } = useAccount();
+ const { uid, recoveryKey, attachedClients, subscriptions } = useAccount();
const ftlMsgResolver = useFtlMsgResolver();
Metrics.setProperties({
@@ -35,6 +38,23 @@ export const PageSettings = (_: RouteComponentProps) => {
GleanMetrics.accountPref.view();
}, []);
+ const [productPromoGleanEventSent, setProductPromoGleanEventSent] =
+ useState(false);
+
+ useEffect(() => {
+ // We want this view event to fire whenever the account settings page view
+ // event fires, if the user is shown the promo.
+ const { gleanEvent, hasAllPromoProducts } = getProductPromoData(
+ attachedClients,
+ subscriptions
+ );
+ if (!hasAllPromoProducts && !productPromoGleanEventSent) {
+ GleanMetrics.accountPref.promoMonitorView(gleanEvent);
+ // Keep track of this because `attachedClients` can change on disconnect
+ setProductPromoGleanEventSent(true);
+ }
+ }, [attachedClients, subscriptions, productPromoGleanEventSent]);
+
const accountRecoveryNotificationProps = {
headerImage: keyImage,
ctaText: ftlMsgResolver.getMsg(
diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx b/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx
index 0a73227fe0..c669c9b408 100644
--- a/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx
+++ b/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx
@@ -6,15 +6,14 @@ import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import ProductPromo, { ProductPromoType } 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 GleanMetrics from '../../../lib/glean';
-
-// List all services this component handles
-const PRODUCT_PROMO_SERVICES = [MozServices.Monitor];
-const PRODUCT_PROMO_SUBSCRIPTIONS = [{ productName: MozServices.MonitorPlus }];
+import {
+ ALL_PRODUCT_PROMO_SERVICES,
+ ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
+} from '../../../pages/mocks';
jest.mock('../../../lib/glean', () => ({
__esModule: true,
@@ -32,12 +31,8 @@ describe('ProductPromo', () => {
});
it('renders nothing if user has Monitor but not MonitorPlus, and MonitorPlus Promo is disabled', () => {
- const services = MOCK_SERVICES.filter((service) =>
- // TODO: MozServices / string discrepancy, FXA-6802
- PRODUCT_PROMO_SERVICES.includes(service.name as MozServices)
- );
const account = {
- attachedClients: services,
+ attachedClients: ALL_PRODUCT_PROMO_SERVICES,
subscriptions: [],
} as unknown as Account;
@@ -48,17 +43,12 @@ describe('ProductPromo', () => {
);
expect(container.firstChild).toBeNull();
- expect(GleanMetrics.accountPref.promoMonitorView).not.toHaveBeenCalled();
});
it('renders nothing if user has all products and subscriptions', 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,
- subscriptions: PRODUCT_PROMO_SUBSCRIPTIONS,
+ attachedClients: ALL_PRODUCT_PROMO_SERVICES,
+ subscriptions: ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
} as unknown as Account;
const { container } = renderWithLocalizationProvider(
@@ -68,7 +58,6 @@ describe('ProductPromo', () => {
);
expect(container.firstChild).toBeNull();
- expect(GleanMetrics.accountPref.promoMonitorView).not.toHaveBeenCalled();
});
it('renders Monitor promo if user does not have Monitor', async () => {
const account = {
@@ -89,9 +78,6 @@ describe('ProductPromo', () => {
'href',
'https://monitor.mozilla.org/?utm_source=moz-account&utm_medium=product-partnership&utm_term=sidebar&utm_content=monitor-free&utm_campaign=settings-promo'
);
- expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledWith({
- event: { reason: 'free' },
- });
});
it('renders Monitor Plus promo if user does not have Monitor Plus', async () => {
@@ -120,9 +106,6 @@ describe('ProductPromo', () => {
'href',
'https://monitor.mozilla.org/#pricing?utm_source=moz-account&utm_medium=product-partnership&utm_term=sidebar&utm_content=monitor-plus&utm_campaign=settings-promo'
);
- expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledWith({
- event: { reason: 'plus' },
- });
});
it('emits metric when user clicks call to action', async () => {
diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx b/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx
index 08a7a15eed..4ff382885d 100644
--- a/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx
+++ b/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx
@@ -8,7 +8,7 @@ import monitorTextLogo from './monitor-text-logo.svg';
import { FtlMsg } from 'fxa-react/lib/utils';
import classNames from 'classnames';
import { MozServices } from '../../../lib/types';
-import { useAccount, useConfig } from '../../../models';
+import { AccountData, useAccount, useConfig } from '../../../models';
import { constructHrefWithUtm } from '../../../lib/utilities';
import { LINK } from '../../../constants';
import GleanMetrics from '../../../lib/glean';
@@ -27,13 +27,12 @@ export interface ProductPromoProps {
monitorPlusEnabled?: boolean;
}
-export const ProductPromo = ({
- type = ProductPromoType.Sidebar,
- monitorPlusEnabled = MONITOR_PLUS_ENABLED,
-}: ProductPromoProps) => {
- const { attachedClients, subscriptions } = useAccount();
- const { env } = useConfig();
-
+export function getProductPromoData(
+ attachedClients: AccountData['attachedClients'],
+ subscriptions: AccountData['subscriptions'],
+ // Temporary until we work on MonitorPlus
+ monitorPlusEnabled = MONITOR_PLUS_ENABLED
+) {
const hasMonitor = attachedClients.some(
({ name }) => name === MozServices.Monitor
);
@@ -42,10 +41,29 @@ export const ProductPromo = ({
({ productName }) => productName === MozServices.MonitorPlus
);
+ // Temporary until we work on MonitorPlus
const showMonitorPlusPromo =
hasMonitor && !hasMonitorPlus && monitorPlusEnabled;
+ const hasAllPromoProducts = hasMonitor && !showMonitorPlusPromo;
- if (hasMonitor && !showMonitorPlusPromo) {
+ const gleanEvent = showMonitorPlusPromo
+ ? { event: { reason: 'plus' } }
+ : { event: { reason: 'free' } };
+
+ return { showMonitorPlusPromo, hasAllPromoProducts, gleanEvent };
+}
+
+export const ProductPromo = ({
+ type = ProductPromoType.Sidebar,
+ // Temporary until we work on MonitorPlus
+ monitorPlusEnabled = MONITOR_PLUS_ENABLED,
+}: ProductPromoProps) => {
+ const { attachedClients, subscriptions } = useAccount();
+ const { env } = useConfig();
+ const { showMonitorPlusPromo, hasAllPromoProducts, gleanEvent } =
+ getProductPromoData(attachedClients, subscriptions, monitorPlusEnabled);
+
+ if (hasAllPromoProducts) {
return <>>;
}
@@ -67,17 +85,6 @@ export const ProductPromo = ({
'settings-promo'
);
- const gleanEvent = showMonitorPlusPromo
- ? { event: { reason: 'plus' } }
- : { event: { reason: 'free' } };
-
- // NOTE, this is a quick fix to prevent double 'view' event firing
- // since we use this component in two places (sidebar + settings).
- // We will want to refactor this to be less fragile.
- if (type === ProductPromoType.Settings) {
- GleanMetrics.accountPref.promoMonitorView(gleanEvent);
- }
-
const promoContent = showMonitorPlusPromo ? (
<>
diff --git a/packages/fxa-settings/src/models/Account.ts b/packages/fxa-settings/src/models/Account.ts
index 77252b6cce..e9fee2517e 100644
--- a/packages/fxa-settings/src/models/Account.ts
+++ b/packages/fxa-settings/src/models/Account.ts
@@ -83,6 +83,11 @@ export interface AttachedClient {
refreshTokenId: string | null;
}
+interface Subscription {
+ created: number;
+ productName: string;
+}
+
export interface AccountData {
uid: hexstring;
displayName: string | null;
@@ -102,10 +107,7 @@ export interface AccountData {
attachedClients: AttachedClient[];
linkedAccounts: LinkedAccount[];
totp: AccountTotp;
- subscriptions: {
- created: number;
- productName: string;
- }[];
+ subscriptions: Subscription[];
securityEvents: SecurityEvent[];
}
diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx
index c427afac30..838f2d3705 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx
@@ -26,7 +26,7 @@ export type SigninPushCodeConfirmProps = {
errorMessage?: string;
};
-const ProductPromotion = () => {
+const Products = () => {
const products = [
{
icon: monitorIcon,
@@ -99,7 +99,7 @@ const LoginApprovedMessage = () => {
Your login has been approved. Please close this window.
-
+
);
};
diff --git a/packages/fxa-settings/src/pages/mocks.tsx b/packages/fxa-settings/src/pages/mocks.tsx
index 15d8e46521..418cd24644 100644
--- a/packages/fxa-settings/src/pages/mocks.tsx
+++ b/packages/fxa-settings/src/pages/mocks.tsx
@@ -66,3 +66,7 @@ export function mockLoadingSpinnerModule() {
}
export const MOCK_RECOVERY_KEY = 'ARJDF300TFEPRJ7SFYB8QVNVYT60WWS2';
export const MOCK_REMOTE_METADATA = JSON.stringify({});
+export const ALL_PRODUCT_PROMO_SERVICES = [{ name: MozServices.Monitor }];
+export const ALL_PRODUCT_PROMO_SUBSCRIPTIONS = [
+ { productName: MozServices.MonitorPlus },
+];