Merge pull request #17335 from mozilla/FXA-10053

feat(settings): Standardize UTM params for Bento Menu links
This commit is contained in:
Valerie Pomerleau 2024-08-07 07:46:27 -07:00 коммит произвёл GitHub
Родитель 4d3bdb02e8 c9f356e204
Коммит db3d150222
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
37 изменённых файлов: 309 добавлений и 142 удалений

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

@ -12,7 +12,7 @@ import LinkExternal from 'fxa-react/components/LinkExternal';
import InputPassword from '../InputPassword';
import PasswordValidator from '../../lib/password-validator';
import { useNavigateWithQuery as useNavigate } from '../../lib/hooks/useNavigateWithQuery';
import { HomePath } from '../../constants';
import { SETTINGS_PATH } from '../../constants';
import { logViewEvent, settingsViewName } from '../../lib/metrics';
type FormPasswordProps = {
@ -69,7 +69,7 @@ export const FormPassword = ({
}: FormPasswordProps) => {
const navigate = useNavigate();
const goHome = useCallback(
() => navigate(HomePath + '#password', { replace: true }),
() => navigate(SETTINGS_PATH + '#password', { replace: true }),
[navigate]
);
const passwordValidator = new PasswordValidator(primaryEmail);

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

@ -5,7 +5,7 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithRouter } from '../../../models/mocks';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { MockSettingsAppLayout } from './mocks';
it('renders the app with children', async () => {
@ -16,7 +16,7 @@ it('renders the app with children', async () => {
<p data-testid="test-child">Hello, world!</p>
</MockSettingsAppLayout>
);
await navigate(HomePath);
await navigate(SETTINGS_PATH);
expect(screen.getByTestId('app')).toBeInTheDocument();
expect(screen.getByTestId('content-skip')).toBeInTheDocument();
expect(screen.getByTestId('header')).toBeInTheDocument();

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

@ -37,6 +37,44 @@ describe('BentoMenu', () => {
expect(screen.queryByTestId(dropDownId)).not.toBeInTheDocument();
});
it('renders the expected product links', () => {
renderWithLocalizationProvider(<BentoMenu />);
fireEvent.click(screen.getByTestId('drop-down-bento-menu-toggle'));
expect(screen.queryByTestId(dropDownId)).toBeInTheDocument();
expect(
screen.getByRole('link', { name: /Firefox Browser for Desktop/ })
).toHaveAttribute(
'href',
'https://www.mozilla.org/firefox/new/?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=bento&utm_content=fx-desktop&utm_campaign=permanent'
);
expect(
screen.getByRole('link', { name: /Firefox Browser for Mobile/ })
).toHaveAttribute(
'href',
'https://www.mozilla.org/firefox/mobile/?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=bento&utm_content=fx-mobile&utm_campaign=permanent'
);
expect(
screen.getByRole('link', { name: /Mozilla Monitor/ })
).toHaveAttribute(
'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'
);
expect(screen.getByRole('link', { name: /Mozilla VPN/ })).toHaveAttribute(
'href',
'https://vpn.mozilla.org/?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=bento&utm_content=vpn&utm_campaign=permanent'
);
});
it('closes on esc keypress', () => {
renderWithLocalizationProvider(<BentoMenu />);

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

@ -18,6 +18,8 @@ import { ReactComponent as BentoIcon } from './bento.svg';
import { ReactComponent as CloseIcon } from '@fxa/shared/assets/images/close.svg';
import { FtlMsg } from 'fxa-react/lib/utils';
import { useFtlMsgResolver } from '../../../models/hooks';
import { LINK } from '../../../constants';
import { constructHrefWithUtm } from '../../../lib/utilities';
export const BentoMenu = () => {
const [isRevealed, setRevealed] = useState(false);
@ -34,6 +36,51 @@ export const BentoMenu = () => {
'Mozilla products'
);
const desktopLink = constructHrefWithUtm(
LINK.FX_DESKTOP,
'mozilla-websites',
'moz-account',
'bento',
'fx-desktop',
'permanent'
);
const mobileLink = constructHrefWithUtm(
LINK.FX_MOBILE,
'mozilla-websites',
'moz-account',
'bento',
'fx-mobile',
'permanent'
);
const monitorLink = constructHrefWithUtm(
LINK.MONITOR,
'mozilla-websites',
'moz-account',
'bento',
'monitor',
'permanent'
);
const relayLink = constructHrefWithUtm(
LINK.RELAY,
'mozilla-websites',
'moz-account',
'bento',
'relay',
'permanent'
);
const vpnLink = constructHrefWithUtm(
LINK.VPN,
'mozilla-websites',
'moz-account',
'bento',
'vpn',
'permanent'
);
return (
<div className="relative self-center flex mx-2" ref={bentoMenuInsideRef}>
<button
@ -75,7 +122,7 @@ export const BentoMenu = () => {
<li>
<LinkExternal
data-testid="desktop-link"
href="https://www.mozilla.org/firefox/new/?utm_source=firefox-accounts&utm_medium=referral&utm_campaign=bento&utm_content=desktop"
href={desktopLink}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
@ -89,7 +136,7 @@ export const BentoMenu = () => {
<li>
<LinkExternal
data-testid="mobile-link"
href="http://mozilla.org/firefox/mobile?utm_source=firefox-accounts&utm_medium=referral&utm_campaign=bento&utm_content=desktop"
href={mobileLink}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
@ -103,7 +150,7 @@ export const BentoMenu = () => {
<li>
<LinkExternal
data-testid="monitor-link"
href="https://monitor.mozilla.org"
href={monitorLink}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
@ -115,7 +162,7 @@ export const BentoMenu = () => {
<li>
<LinkExternal
data-testid="relay-link"
href="https://relay.firefox.com/"
href={relayLink}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
@ -129,7 +176,7 @@ export const BentoMenu = () => {
<li>
<LinkExternal
data-testid="vpn-link"
href="https://vpn.mozilla.org/?utm_source=accounts.firefox.com&utm_medium=referral&utm_campaign=fxa-settings&utm_content=bento-promo"
href={vpnLink}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>

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

@ -16,7 +16,7 @@ import { LockImage } from '../../images';
import Banner, { BannerType } from '../../Banner';
import { RecoveryKeyAction } from '../PageRecoveryKeyCreate';
import { Link } from '@reach/router';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { getLocalizedErrorMessage } from '../../../lib/error-utils';
type FormData = {
@ -177,7 +177,7 @@ export const FlowRecoveryKeyConfirmPwd = ({
<FtlMsg id="flow-recovery-key-info-cancel-link">
<Link
className="link-blue text-sm mx-auto"
to={HomePath}
to={SETTINGS_PATH}
onClick={() => {
logViewEvent(`flow.${viewName}`, 'change-key.cancel');
}}

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

@ -11,7 +11,7 @@ import { FtlMsg } from 'fxa-react/lib/utils';
import { logViewEvent } from '../../../lib/metrics';
import { RecoveryKeyAction } from '../PageRecoveryKeyCreate';
import { Link } from '@reach/router';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
export type FlowRecoveryKeyInfoProps = {
action?: RecoveryKeyAction;
@ -103,7 +103,7 @@ export const FlowRecoveryKeyInfo = ({
<FtlMsg id="flow-recovery-key-info-cancel-link">
<Link
className="link-blue text-sm mx-auto mt-4"
to={HomePath}
to={SETTINGS_PATH}
onClick={() =>
logViewEvent(`flow.${viewName}`, 'change-key.cancel')
}

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

@ -12,7 +12,7 @@ import { useAccount, useFtlMsgResolver } from '../../../models';
import { useBooleanState } from 'fxa-react/lib/hooks';
import { useLocation } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import {
LinkedAccountProviderIds,
UnlinkAccountLocationState,
@ -42,7 +42,7 @@ export function LinkedAccount({
// Keep the user where they were, but update router state
const resetLocationState = () =>
navigate(HomePath + '#linked-accounts', {
navigate(SETTINGS_PATH + '#linked-accounts', {
replace: true,
state: { wantsUnlinkProviderId: undefined },
});
@ -73,7 +73,7 @@ export function LinkedAccount({
// If a user doesn't have a password, they must create one first. We send
// a navigation state that's passed back to Settings on password create
// success that we account for here by automatically re-opening the modal.
navigate(HomePath + '/create_password', {
navigate(SETTINGS_PATH + '/create_password', {
state: { wantsUnlinkProviderId: providerId },
});
}

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

@ -6,7 +6,7 @@ import 'mutationobserver-shim';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { Account, AppContext } from '../../../models';
import { Config } from '../../../lib/config';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { typeByTestIdFn } from '../../../lib/test-utils';
import {
MOCK_ACCOUNT,
@ -156,7 +156,7 @@ it('allows user to finish', async () => {
await waitFor(() =>
expect(mockNavigate).toHaveBeenCalledWith(
HomePath + '#two-step-authentication',
SETTINGS_PATH + '#two-step-authentication',
{ replace: true }
)
);

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

@ -10,7 +10,7 @@ import FlowContainer from '../FlowContainer';
import VerifiedSessionGuard from '../VerifiedSessionGuard';
import DataBlock from '../../DataBlock';
import InputText from '../../InputText';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import {
useAccount,
useAlertBar,
@ -31,7 +31,7 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
const ftlMsgResolver = useFtlMsgResolver();
const goHome = () =>
navigate(HomePath + '#two-step-authentication', { replace: true });
navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true });
const [subtitle, setSubtitle] = useState<string>(
ftlMsgResolver.getMsg('tfa-replace-code-1-2', 'Step 1 of 2')
@ -47,7 +47,7 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
'Account backup authentication codes updated'
)
);
navigate(HomePath + '#two-step-authentication', { replace: true });
navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true });
};
const onRecoveryCodeSubmit = async (_: RecoveryCodeForm) => {

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

@ -7,7 +7,7 @@ import React, { useCallback, useRef } from 'react';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import { Localized, useLocalization } from '@fluent/react';
import { useAccount, useAlertBar } from '../../../models';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import ButtonIcon from '../ButtonIcon';
import { ReactComponent as AddIcon } from './add.svg';
@ -35,7 +35,7 @@ export const RemovePhotoBtn = () => {
const deleteAvatar = useCallback(async () => {
try {
await account.deleteAvatar();
navigate(HomePath, { replace: true });
navigate(SETTINGS_PATH, { replace: true });
} catch (err) {
alertBar.error(
l10n.getString(
@ -149,7 +149,7 @@ export const ConfirmBtns = ({
<Localized id="avatar-page-cancel-button">
<button
className="cta-neutral cta-base-p mx-2 flex-1"
onClick={() => navigate(HomePath, { replace: true })}
onClick={() => navigate(SETTINGS_PATH, { replace: true })}
data-testid="close-button"
>
Cancel

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

@ -15,7 +15,7 @@ import 'react-easy-crop/react-easy-crop.css';
import { isMobileDevice } from '../../../lib/utilities';
import { useAccount, useAlertBar } from '../../../models';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { onFileChange } from '../../../lib/file-utils';
import { getCroppedImg } from '../../../lib/canvas-utils';
import {
@ -77,7 +77,7 @@ export const PageAddAvatar = (_: RouteComponentProps) => {
try {
await account.uploadAvatar(file);
logViewEvent(settingsViewName, 'avatar.crop.submit.change');
navigate(HomePath + '#profile-picture', { replace: true });
navigate(SETTINGS_PATH + '#profile-picture', { replace: true });
} catch (e) {
onFileError();
}

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

@ -5,7 +5,7 @@
import 'mutationobserver-shim';
import React from 'react';
import { act, fireEvent, screen } from '@testing-library/react';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { mockAppContext, renderWithRouter } from '../../../models/mocks';
import PageChangePassword from '.';
import {
@ -93,7 +93,7 @@ it('shows an error when old and new password are the same', async () => {
it('redirects on success', async () => {
await changePassword();
expect(mockNavigate).toHaveBeenCalledWith(HomePath + '#password', {
expect(mockNavigate).toHaveBeenCalledWith(SETTINGS_PATH + '#password', {
replace: true,
});
});

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

@ -6,7 +6,7 @@ import React, { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
import { RouteComponentProps } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import {
logViewEvent,
settingsViewName,
@ -57,7 +57,7 @@ export const PageChangePassword = ({}: RouteComponentProps) => {
alertBar.success(
l10n.getString('pw-change-success-alert-2', null, 'Password updated')
);
navigate(HomePath + '#password', { replace: true });
navigate(SETTINGS_PATH + '#password', { replace: true });
}, [alertBar, l10n, navigate]);
const onFormSubmit = useCallback(

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

@ -20,7 +20,7 @@ import {
inputVerifyPassword,
} from '../../FormPassword/index.test';
import { act, fireEvent, screen } from '@testing-library/react';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { SettingsContext } from '../../../models/contexts/SettingsContext';
import { LinkedAccountProviderIds } from '../../../lib/types';
@ -161,7 +161,7 @@ describe('PageCreatePassword', () => {
it('redirects and displays alert bar on success', async () => {
const alertBarInfo = await createPassword();
expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(HomePath + '#password', {
expect(mockNavigate).toHaveBeenCalledWith(SETTINGS_PATH + '#password', {
replace: true,
});
expect(alertBarInfo.success).toHaveBeenCalledTimes(1);
@ -177,7 +177,7 @@ describe('PageCreatePassword', () => {
wantsUnlinkProviderId: LinkedAccountProviderIds.Google,
};
await createPassword();
expect(mockNavigate).toHaveBeenCalledWith(HomePath + '#password', {
expect(mockNavigate).toHaveBeenCalledWith(SETTINGS_PATH + '#password', {
replace: true,
state: {
wantsUnlinkProviderId: LinkedAccountProviderIds.Google,

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

@ -7,7 +7,7 @@ import { RouteComponentProps, useLocation } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import React, { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import {
logViewEvent,
settingsViewName,
@ -54,7 +54,7 @@ export const PageCreatePassword = ({}: RouteComponentProps) => {
alertBar.success(
ftlMsgResolver.getMsg('pw-create-success-alert-2', 'Password set')
);
navigate(HomePath + '#password', {
navigate(SETTINGS_PATH + '#password', {
replace: true,
...(wantsUnlinkProviderId ? { state: { wantsUnlinkProviderId } } : {}),
});

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

@ -10,18 +10,7 @@ import { useAccount, useAlertBar } from '../../../models';
import InputPassword from '../../InputPassword';
import FlowContainer from '../FlowContainer';
import VerifiedSessionGuard from '../VerifiedSessionGuard';
import {
AddonsLink,
HomePath,
HubsLink,
MDNLink,
MonitorLink,
PocketLink,
RelayLink,
ROOTPATH,
SyncLink,
VPNLink,
} from '../../../constants';
import { LINK, SETTINGS_PATH } from '../../../constants';
import { logViewEvent, usePageViewEvent } from '../../../lib/metrics';
import { Checkbox } from '../Checkbox';
import { useLocalization } from '@fluent/react';
@ -51,42 +40,42 @@ const deleteProducts = [
{
localizationId: 'delete-account-product-firefox-sync',
productName: 'Syncing Firefox data',
href: SyncLink,
href: LINK.FX_SYNC,
},
{
localizationId: 'delete-account-product-mozilla-vpn',
productName: 'Mozilla VPN',
href: VPNLink,
href: LINK.VPN,
},
{
localizationId: 'delete-account-product-firefox-relay',
productName: 'Firefox Relay',
href: RelayLink,
href: LINK.RELAY,
},
{
localizationId: 'delete-account-product-firefox-addons',
productName: 'Firefox Add-ons',
href: AddonsLink,
href: LINK.AMO,
},
{
localizationId: 'delete-account-product-mozilla-monitor',
productName: 'Mozilla Monitor',
href: MonitorLink,
href: LINK.MONITOR,
},
{
localizationId: 'delete-account-product-mdn-plus',
productName: 'MDN Plus',
href: MDNLink,
href: LINK.MDN,
},
{
localizationId: 'delete-account-product-mozilla-hubs',
productName: 'Mozilla Hubs',
href: HubsLink,
href: LINK.HUBS,
},
{
localizationId: 'delete-account-product-pocket',
productName: 'Pocket',
href: PocketLink,
href: LINK.POCKET,
},
];
@ -152,7 +141,7 @@ export const PageDeleteAccount = (_: RouteComponentProps) => {
'flow.settings.account-delete',
'confirm-password.success'
);
hardNavigate(ROOTPATH, { delete_account_success: true }, true);
hardNavigate('/', { delete_account_success: true }, true);
} catch (e) {
const localizedError = l10n.getString(
getErrorFtlId(AuthUiErrors.INCORRECT_PASSWORD),
@ -290,7 +279,9 @@ export const PageDeleteAccount = (_: RouteComponentProps) => {
<button
className="cta-neutral mx-2 px-10 py-2"
onClick={() =>
navigate(HomePath + '#delete-account', { replace: true })
navigate(SETTINGS_PATH + '#delete-account', {
replace: true,
})
}
data-testid="cancel-button"
>

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

@ -11,7 +11,7 @@ import {
mockSettingsContext,
renderWithRouter,
} from '../../../models/mocks';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { Account, AppContext } from '../../../models';
import { SettingsContext } from '../../../models/contexts/SettingsContext';
@ -90,7 +90,7 @@ it('navigates back to settings home and shows a success message on a successful
</AppContext.Provider>
);
await submitDisplayName('John Hope');
expect(history.location.pathname).toBe(HomePath);
expect(history.location.pathname).toBe(SETTINGS_PATH);
expect(alertBarInfo.success).toHaveBeenCalledTimes(1);
expect(alertBarInfo.success).toHaveBeenCalledWith('Display name updated');
});

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

@ -8,7 +8,7 @@ import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavig
import { useForm } from 'react-hook-form';
import FlowContainer from '../FlowContainer';
import InputText from '../../InputText';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { Localized, useLocalization } from '@fluent/react';
import { useAccount, useAlertBar } from '../../../models';
@ -21,7 +21,7 @@ export const PageDisplayName = (_: RouteComponentProps) => {
const alertBar = useAlertBar();
const { l10n } = useLocalization();
const navigate = useNavigate();
const goHome = () => navigate(HomePath, { replace: true });
const goHome = () => navigate(SETTINGS_PATH, { replace: true });
const alertSuccessAndGoHome = useCallback(() => {
alertBar.success(
l10n.getString(
@ -30,7 +30,7 @@ export const PageDisplayName = (_: RouteComponentProps) => {
'Display name updated'
)
);
navigate(HomePath, { replace: true });
navigate(SETTINGS_PATH, { replace: true });
}, [alertBar, l10n, navigate]);
const initialValue = account.displayName || '';
const { register, handleSubmit, formState, trigger } = useForm<{

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

@ -5,7 +5,7 @@
import React, { useState } from 'react';
import { RouteComponentProps } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { usePageViewEvent } from '../../../lib/metrics';
import { useAccount, useFtlMsgResolver } from '../../../models';
import FlowRecoveryKeyConfirmPwd from '../FlowRecoveryKeyConfirmPwd';
@ -35,7 +35,8 @@ export const PageRecoveryKeyCreate = (props: RouteComponentProps) => {
const action = recoveryKey
? RecoveryKeyAction.Change
: RecoveryKeyAction.Create;
const goHome = () => navigate(HomePath + '#recovery-key', { replace: true });
const goHome = () =>
navigate(SETTINGS_PATH + '#recovery-key', { replace: true });
const localizedPageTitle = ftlMsgResolver.getMsg(
'recovery-key-create-page-title',
@ -48,7 +49,7 @@ export const PageRecoveryKeyCreate = (props: RouteComponentProps) => {
);
const navigateBackward = () => {
navigate(HomePath);
navigate(SETTINGS_PATH);
};
const navigateForward = (e?: React.MouseEvent<HTMLElement>) => {
@ -56,7 +57,7 @@ export const PageRecoveryKeyCreate = (props: RouteComponentProps) => {
if (currentStep + 1 <= numberOfSteps) {
setCurrentStep(currentStep + 1);
} else {
navigate(HomePath);
navigate(SETTINGS_PATH);
}
};

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

@ -3,7 +3,7 @@ import { Localized, useLocalization } from '@fluent/react';
import { RouteComponentProps } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import { logViewEvent, usePageViewEvent } from '../../../lib/metrics';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import InputText from '../../InputText';
import FlowContainer from '../FlowContainer';
import VerifiedSessionGuard from '../VerifiedSessionGuard';
@ -29,7 +29,7 @@ export const PageSecondaryEmailAdd = (_: RouteComponentProps) => {
const alertBar = useAlertBar();
const account = useAccount();
const goHome = () =>
navigate(HomePath + '#secondary-email', { replace: true });
navigate(SETTINGS_PATH + '#secondary-email', { replace: true });
const createSecondaryEmail = useCallback(
async (email: string) => {

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

@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { Localized, useLocalization } from '@fluent/react';
import { RouteComponentProps } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { logViewEvent } from '../../../lib/metrics';
import { useAccount, useAlertBar } from '../../../models';
import InputText from '../../InputText';
@ -26,7 +26,7 @@ export const PageSecondaryEmailVerify = ({ location }: RouteComponentProps) => {
});
const navigate = useNavigate();
const goHome = useCallback(
() => navigate(HomePath + '#secondary-email', { replace: true }),
() => navigate(SETTINGS_PATH + '#secondary-email', { replace: true }),
[navigate]
);
const { l10n } = useLocalization();
@ -41,7 +41,7 @@ export const PageSecondaryEmailVerify = ({ location }: RouteComponentProps) => {
`${email} successfully added`
)
);
navigate(HomePath + '#secondary-email', { replace: true });
navigate(SETTINGS_PATH + '#secondary-email', { replace: true });
},
[alertBar, l10n, navigate]
);
@ -84,7 +84,7 @@ export const PageSecondaryEmailVerify = ({ location }: RouteComponentProps) => {
useEffect(() => {
if (!email) {
navigate(HomePath, { replace: true });
navigate(SETTINGS_PATH, { replace: true });
}
}, [email, navigate]);

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

@ -11,7 +11,7 @@ import LinkedAccounts from '../LinkedAccounts';
import * as Metrics from '../../../lib/metrics';
import { useAccount } from '../../../models';
import { DeleteAccountPath } from 'fxa-settings/src/constants';
import { SETTINGS_PATH } from 'fxa-settings/src/constants';
import { Localized } from '@fluent/react';
import DataCollection from '../DataCollection';
import GleanMetrics from '../../../lib/glean';
@ -62,7 +62,7 @@ export const PageSettings = (_: RouteComponentProps) => {
<Link
data-testid="settings-delete-account"
className="cta-caution text-sm transition-standard mt-12 py-2 px-5 mobileLandscape:py-1"
to={DeleteAccountPath}
to={SETTINGS_PATH + '/delete_account'}
onClick={() => GleanMetrics.deleteAccount.settingsSubmit()}
>
Delete account

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

@ -13,7 +13,7 @@ import {
import React from 'react';
import PageTwoStepAuthentication, { metricsPreInPostFix } from '.';
import { checkCode, getCode } from '../../../lib/totp';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import * as Metrics from '../../../lib/metrics';
import { Account, AppContext } from '../../../models';
import { AuthUiErrors } from 'fxa-settings/src/lib/auth-errors/auth-errors';
@ -286,7 +286,7 @@ describe('step 3', () => {
expect(getCode).toBeCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(
HomePath + '#two-step-authentication',
SETTINGS_PATH + '#two-step-authentication',
{ replace: true }
);
expect(alertBarInfo.success).toHaveBeenCalledTimes(1);

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

@ -13,7 +13,7 @@ import VerifiedSessionGuard from '../VerifiedSessionGuard';
import DataBlock from '../../DataBlock';
import { useAccount, useAlertBar, useSession } from '../../../models';
import { checkCode, copyRecoveryCodes, getCode } from '../../../lib/totp';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { logViewEvent, useMetrics } from '../../../lib/metrics';
import { Localized, useLocalization } from '@fluent/react';
import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors';
@ -32,12 +32,12 @@ export const PageTwoStepAuthentication = (_: RouteComponentProps) => {
const { l10n } = useLocalization();
const alertBar = useAlertBar();
const goHome = () =>
navigate(HomePath + '#two-step-authentication', { replace: true });
navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true });
const alertSuccessAndGoHome = useCallback(() => {
alertBar.success(
l10n.getString('tfa-enabled', null, 'Two-step authentication enabled')
);
navigate(HomePath + '#two-step-authentication', { replace: true });
navigate(SETTINGS_PATH + '#two-step-authentication', { replace: true });
}, [alertBar, l10n, navigate]);
const totpForm = useForm<TotpForm>({

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

@ -4,13 +4,12 @@
import React from 'react';
import { screen } from '@testing-library/react';
import ProductPromo from '.';
import ProductPromo, { monitorPromoLink } 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];
@ -49,7 +48,7 @@ describe('ProductPromo', () => {
);
expect(screen.getByRole('link', { name: /Get free scan/ })).toHaveAttribute(
'href',
MonitorLink
monitorPromoLink
);
});
});

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

@ -7,9 +7,10 @@ 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';
import { constructHrefWithUtm } from '../../../lib/utilities';
import { LINK } from '../../../constants';
export enum ProductPromoType {
Sidebar = 'sidebar',
@ -21,6 +22,15 @@ export interface ProductPromoProps {
// product?: MozServices;
}
export const monitorPromoLink = constructHrefWithUtm(
LINK.MONITOR,
'product-partnership',
'moz-account',
'sidebar',
'monitor-free',
'settings-promo'
);
export const ProductPromo = ({
type = ProductPromoType.Sidebar,
}: ProductPromoProps) => {
@ -29,6 +39,7 @@ export const ProductPromo = ({
const hasMonitor = attachedClients.some(
({ name }) => name === MozServices.Monitor
);
// const hasMonitorPlus = subscriptions.some(
// ({ productName }) => productName === MozServices.MonitorPlus
// );
@ -73,7 +84,7 @@ export const ProductPromo = ({
</FtlMsg>
</p>
{/* possible todo, link to their stage env in stage? can do with FXA-10147 */}
<LinkExternal href={MonitorLink} className="link-blue">
<LinkExternal href={monitorPromoLink} className="link-blue">
<FtlMsg id="product-promo-monitor-cta">Get free scan</FtlMsg>
</LinkExternal>
</div>

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

@ -6,7 +6,7 @@ import React, { forwardRef } from 'react';
import { useAccount } from '../../../models';
import { UnitRow } from '../UnitRow';
import { UnitRowSecondaryEmail } from '../UnitRowSecondaryEmail';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { FtlMsg } from 'fxa-react/lib/utils';
import GleanMetrics from '../../../lib/glean';
@ -31,7 +31,7 @@ export const Profile = forwardRef<HTMLDivElement>((_, ref) => {
header="Picture"
headerId="profile-picture"
headerValue={!avatar.isDefault}
route={`${HomePath}/avatar`}
route={`${SETTINGS_PATH}/avatar`}
prefixDataTestId="avatar"
{...{ avatar }}
/>

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

@ -7,7 +7,7 @@ import { screen } from '@testing-library/react';
import UnitRow from '.';
import { renderWithRouter } from '../../../models/mocks';
import { Account, AppContext } from '../../../models';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
describe('UnitRow', () => {
it('renders as expected with minimal required attributes', () => {
@ -133,7 +133,7 @@ describe('UnitRow', () => {
header="Picture"
headerId="profile-picture"
headerValue={!account.avatar.isDefault}
route={`${HomePath}/avatar`}
route={`${SETTINGS_PATH}/avatar`}
avatar={account.avatar}
/>
</AppContext.Provider>
@ -158,7 +158,7 @@ describe('UnitRow', () => {
header="Picture"
headerId="profile-picture"
headerValue={!account.avatar.isDefault}
route={`${HomePath}/avatar`}
route={`${SETTINGS_PATH}/avatar`}
avatar={account.avatar}
/>
</AppContext.Provider>

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

@ -4,7 +4,7 @@
import React from 'react';
import { useBooleanState } from 'fxa-react/lib/hooks';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { UnitRow } from '.';
import { Modal } from '../Modal';
import { AppContext } from 'fxa-settings/src/models';
@ -82,7 +82,7 @@ export const SubjectWithDefaultAvatar = () => {
header="Picture"
headerId="profile-picture"
headerValue={!avatar.isDefault}
route={`${HomePath}/avatar`}
route={`${SETTINGS_PATH}/avatar`}
{...{ avatar }}
/>
</AppContext.Provider>
@ -101,7 +101,7 @@ export const SubjectWithCustomAvatar = () => {
header="Picture"
headerId="profile-picture"
headerValue={!avatar.isDefault}
route={`${HomePath}/avatar`}
route={`${SETTINGS_PATH}/avatar`}
{...{ avatar }}
/>
</AppContext.Provider>

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

@ -10,7 +10,7 @@ import Modal from '../Modal';
import UnitRow from '../UnitRow';
import VerifiedSessionGuard from '../VerifiedSessionGuard';
import { ButtonIconTrash } from '../ButtonIcon';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { FtlMsg } from 'fxa-react/lib/utils';
import GleanMetrics from '../../../lib/glean';
@ -64,7 +64,7 @@ export const UnitRowRecoveryKey = () => {
? ftlMsgResolver.getMsg('rk-enabled', 'Enabled')
: ftlMsgResolver.getMsg('rk-not-set', 'Not Set')
}
route={`${HomePath}/account_recovery`}
route={`${SETTINGS_PATH}/account_recovery`}
ctaText={
recoveryKey
? ftlMsgResolver.getMsg('rk-action-change-button', 'Change')

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

@ -9,7 +9,7 @@ import UnitRow from '../UnitRow';
import ModalVerifySession from '../ModalVerifySession';
import { ButtonIconTrash, ButtonIconReload } from '../ButtonIcon';
import { Localized, useLocalization } from '@fluent/react';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import GleanMetrics from '../../../lib/glean';
type UnitRowSecondaryEmailContentAndActionsProps = {
@ -34,7 +34,7 @@ export const UnitRowSecondaryEmail = () => {
async (email: string) => {
try {
await account.resendEmailCode(email);
navigate(`${HomePath}/emails/verify`, { state: { email } });
navigate(`${SETTINGS_PATH}/emails/verify`, { state: { email } });
} catch (e) {
alertBar.error(
l10n.getString(
@ -136,7 +136,7 @@ export const UnitRowSecondaryEmail = () => {
headerId="secondary-email"
prefixDataTestId="secondary-email"
headerValue={null}
route={`${HomePath}/emails`}
route={`${SETTINGS_PATH}/emails`}
ctaOnClickAction={() => GleanMetrics.accountPref.secondaryEmailSubmit()}
{...{
alertBarRevealed: alertBar.visible,

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

@ -10,11 +10,11 @@ import UnitRow from '../UnitRow';
import VerifiedSessionGuard from '../VerifiedSessionGuard';
import { useAccount, useAlertBar } from '../../../models';
import { ButtonIconReload } from '../ButtonIcon';
import { HomePath } from '../../../constants';
import { SETTINGS_PATH } from '../../../constants';
import { Localized, useLocalization } from '@fluent/react';
import GleanMetrics from '../../../lib/glean';
const route = `${HomePath}/two_step_authentication`;
const route = `${SETTINGS_PATH}/two_step_authentication`;
const replaceCodesRoute = `${route}/replace_codes`;
export const UnitRowTwoStepAuth = () => {

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

@ -15,7 +15,7 @@ import {
} from '../../models/mocks';
import { Config } from '../../lib/config';
import * as NavTiming from 'fxa-shared/metrics/navigation-timing';
import { HomePath } from '../../constants';
import { SETTINGS_PATH } from '../../constants';
import AppLocalizationProvider from 'fxa-react/lib/AppLocalizationProvider';
import { Subject, createMockSettingsIntegration } from './mocks';
@ -115,10 +115,10 @@ describe('App component', () => {
getByTestId,
history: { navigate },
} = renderWithRouter(<Subject />, {
route: HomePath,
route: SETTINGS_PATH,
});
await navigate(HomePath);
await navigate(SETTINGS_PATH);
expect(getByTestId('settings-profile')).toBeInTheDocument();
});
@ -127,9 +127,9 @@ describe('App component', () => {
const {
getByTestId,
history: { navigate },
} = renderWithRouter(<Subject />, { route: HomePath });
} = renderWithRouter(<Subject />, { route: SETTINGS_PATH });
await navigate(HomePath + '/display_name');
await navigate(SETTINGS_PATH + '/display_name');
expect(getByTestId('input-label')).toHaveTextContent('Enter display name');
});
@ -139,10 +139,10 @@ describe('App component', () => {
getAllByTestId,
history: { navigate },
} = renderWithRouter(<Subject />, {
route: HomePath,
route: SETTINGS_PATH,
});
await navigate(HomePath + '/avatar');
await navigate(SETTINGS_PATH + '/avatar');
expect(getAllByTestId('avatar-nondefault')[0]).toBeInTheDocument();
});
@ -156,10 +156,10 @@ describe('App component', () => {
<AppContext.Provider value={mockAppContext({ session })}>
<Subject />
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
);
await navigate(HomePath + '/change_password');
await navigate(SETTINGS_PATH + '/change_password');
expect(getByTestId('change-password-requirements')).toBeInTheDocument();
});
@ -173,10 +173,10 @@ describe('App component', () => {
<AppContext.Provider value={mockAppContext({ session })}>
<Subject />
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
);
await navigate(HomePath + '/emails');
await navigate(SETTINGS_PATH + '/emails');
expect(getByTestId('secondary-email-input')).toBeInTheDocument();
});
@ -190,10 +190,10 @@ describe('App component', () => {
<AppContext.Provider value={mockAppContext({ session })}>
<Subject />
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
);
await navigate(HomePath + '/emails/verify');
await navigate(SETTINGS_PATH + '/emails/verify');
expect(getByTestId('secondary-email-verify-form')).toBeInTheDocument();
});
@ -207,10 +207,10 @@ describe('App component', () => {
<AppContext.Provider value={mockAppContext({ session })}>
<Subject />
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
);
await navigate(HomePath + '/two_step_authentication');
await navigate(SETTINGS_PATH + '/two_step_authentication');
expect(getByTestId('totp-input')).toBeInTheDocument();
});
@ -224,10 +224,10 @@ describe('App component', () => {
<AppContext.Provider value={mockAppContext({ session })}>
<Subject />
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
);
await navigate(HomePath + '/two_step_authentication/replace_codes');
await navigate(SETTINGS_PATH + '/two_step_authentication/replace_codes');
expect(getByTestId('2fa-recovery-codes')).toBeInTheDocument();
});
@ -241,10 +241,10 @@ describe('App component', () => {
<AppContext.Provider value={mockAppContext({ session })}>
<Subject />
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
);
await navigate(HomePath + '/delete_account');
await navigate(SETTINGS_PATH + '/delete_account');
expect(getByTestId('delete-account-confirm')).toBeInTheDocument();
});
@ -254,10 +254,10 @@ describe('App component', () => {
history,
history: { navigate },
} = renderWithRouter(<Subject />, {
route: HomePath,
route: SETTINGS_PATH,
});
await navigate(HomePath + '/clients');
await navigate(SETTINGS_PATH + '/clients');
expect(history.location.pathname).toBe('/settings#connected-services');
});
@ -267,10 +267,10 @@ describe('App component', () => {
history,
history: { navigate },
} = renderWithRouter(<Subject />, {
route: HomePath,
route: SETTINGS_PATH,
});
await navigate(HomePath + '/avatar/change');
await navigate(SETTINGS_PATH + '/avatar/change');
expect(history.location.pathname).toBe('/settings/avatar');
});
@ -279,9 +279,9 @@ describe('App component', () => {
const {
history,
history: { navigate },
} = renderWithRouter(<Subject />, { route: HomePath });
} = renderWithRouter(<Subject />, { route: SETTINGS_PATH });
await navigate(HomePath + '/create_password');
await navigate(SETTINGS_PATH + '/create_password');
expect(history.location.pathname).toBe('/settings/change_password');
});
@ -308,29 +308,29 @@ describe('App component', () => {
<Subject />
</AppLocalizationProvider>
</AppContext.Provider>,
{ route: HomePath }
{ route: SETTINGS_PATH }
));
});
it('redirects PageRecoveryKeyCreate', async () => {
await history.navigate(HomePath + '/account_recovery');
await history.navigate(SETTINGS_PATH + '/account_recovery');
expect(history.location.pathname).toBe('/settings');
});
it('redirects PageTwoStepAuthentication', async () => {
await history.navigate(HomePath + '/two_step_authentication');
await history.navigate(SETTINGS_PATH + '/two_step_authentication');
expect(history.location.pathname).toBe('/settings');
});
it('redirects Page2faReplaceRecoveryCodes', async () => {
await history.navigate(
HomePath + '/two_step_authentication/replace_codes'
SETTINGS_PATH + '/two_step_authentication/replace_codes'
);
expect(history.location.pathname).toBe('/settings');
});
it('redirects ChangePassword', async () => {
await history.navigate(HomePath + '/change_password');
await history.navigate(SETTINGS_PATH + '/change_password');
expect(history.location.pathname).toBe('/settings/create_password');
});
});

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

@ -24,7 +24,7 @@ import PageTwoStepAuthentication from './PageTwoStepAuthentication';
import { Page2faReplaceRecoveryCodes } from './Page2faReplaceRecoveryCodes';
import { PageDeleteAccount } from './PageDeleteAccount';
import { ScrollToTop } from './ScrollToTop';
import { HomePath } from '../../constants';
import { SETTINGS_PATH } from '../../constants';
import { observeNavigationTiming } from 'fxa-shared/metrics/navigation-timing';
import PageAvatar from './PageAvatar';
import PageRecentActivity from './PageRecentActivity';
@ -79,7 +79,7 @@ export const Settings = ({
return (
<AppLayout {...{ integration }}>
<Head />
<Router basepath={HomePath}>
<Router basepath={SETTINGS_PATH}>
<ScrollToTop default>
<PageSettings path="/" />
<PageDisplayName path="/display_name" />

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

@ -2,17 +2,8 @@
* 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/. */
export const ROOTPATH = '/';
export const HomePath = '/settings';
export const DeleteAccountPath = '/settings/delete_account';
export const VPNLink = 'https://vpn.mozilla.org/';
export const MonitorLink = 'https://monitor.mozilla.org/';
export const SyncLink = 'https://www.mozilla.org/firefox/sync/';
export const RelayLink = 'https://relay.firefox.com/';
export const AddonsLink = 'https://addons.mozilla.org/';
export const MDNLink = 'https://developer.mozilla.org/';
export const HubsLink = 'https://hubs.mozilla.com/';
export const PocketLink = 'https://getpocket.com/';
export const SETTINGS_PATH = '/settings';
export const SHOW_BALLOON_TIMEOUT = 500;
export const HIDE_BALLOON_TIMEOUT = 400;
export const POLLING_INTERVAL_MS = 2000;
@ -31,6 +22,19 @@ export enum ENTRYPOINTS {
FIREFOX_FX_VIEW_ENTRYPOINT = 'fx-view',
}
export enum LINK {
AMO = 'https://addons.mozilla.org/',
FX_DESKTOP = 'https://www.mozilla.org/firefox/new/',
FX_MOBILE = 'https://www.mozilla.org/firefox/mobile/',
FX_SYNC = 'https://www.mozilla.org/firefox/sync/',
HUBS = 'https://hubs.mozilla.com/',
MDN = 'https://developer.mozilla.org/',
MONITOR = 'https://monitor.mozilla.org/',
POCKET = 'https://getpocket.com/',
RELAY = 'https://relay.firefox.com/',
VPN = 'https://vpn.mozilla.org/',
}
// DISPLAY_SAFE_UNICODE regex matches validation used for auth_server
// Match display-safe unicode characters.
// We're pretty liberal with what's allowed in a unicode string,

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

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
constructHrefWithUtm,
deepMerge,
isBase32Crockford,
once,
@ -147,3 +148,49 @@ describe('once', () => {
expect(count).toEqual(2);
});
});
describe('constructHrefWithUtm', () => {
test('should construct URL with given UTM parameters', () => {
const pathname = 'https://example.com';
const utmMedium = 'mozilla-websites';
const utmSource = 'moz-account';
const utmTerm = 'bento';
const utmContent = 'fx-desktop';
const utmCampaign = 'permanent';
const result = constructHrefWithUtm(
pathname,
utmMedium,
utmSource,
utmTerm,
utmContent,
utmCampaign
);
const expected =
'https://example.com?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=bento&utm_content=fx-desktop&utm_campaign=permanent';
expect(result).toBe(expected);
});
test('should construct URL with different UTM parameters', () => {
const pathname = 'https://example.com';
const utmMedium = 'product-partnership';
const utmSource = 'moz-account';
const utmTerm = 'sidebar';
const utmContent = 'vpn';
const utmCampaign = 'connect-device';
const result = constructHrefWithUtm(
pathname,
utmMedium,
utmSource,
utmTerm,
utmContent,
utmCampaign
);
const expected =
'https://example.com?utm_source=moz-account&utm_medium=product-partnership&utm_term=sidebar&utm_content=vpn&utm_campaign=connect-device';
expect(result).toBe(expected);
});
});

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

@ -129,3 +129,32 @@ export function once(key: string, callback: () => void) {
export function resetOnce() {
calls = new Set<string>();
}
/**
* Constructs a URL with UTM parameters appended to the query string.
*
* @param {string} pathname - The base URL path.
* @param {'mozilla-websites' | 'product-partnership'} utmMedium - The medium through which the link is being shared.
* @param {'moz-account'} utmSource - The source of the traffic.
* @param {'bento' | 'sidebar'} utmTerm - The search term or keyword associated with the campaign.
* @param {'fx-desktop' | 'fx-mobile' | 'monitor' | 'monitor-free' | 'monitor-plus' | 'relay' | 'vpn'} utmContent - The specific content or product that the link is associated with.
* @param {'permanent' | 'settings-promo' | 'connect-device'} utmCampaign - The name of the marketing campaign.
* @returns {string} - The constructed URL with UTM parameters.
*/
export const constructHrefWithUtm = (
pathname: string,
utmMedium: 'mozilla-websites' | 'product-partnership',
utmSource: 'moz-account',
utmTerm: 'bento' | 'sidebar',
utmContent:
| 'fx-desktop'
| 'fx-mobile'
| 'monitor'
| 'monitor-free'
| 'monitor-plus'
| 'relay'
| 'vpn',
utmCampaign: 'permanent' | 'settings-promo' | 'connect-device'
) => {
return `${pathname}?utm_source=${utmSource}&utm_medium=${utmMedium}&utm_term=${utmTerm}&utm_content=${utmContent}&utm_campaign=${utmCampaign}`;
};