Merge pull request #18280 from mozilla/FXA-7803

feat(next): Duplicate right-hand portion of fxa-settings header
Because:

* The fxa-settings header contains functionality around upsells and cross-sells, and gives the user direct access to their signed-in status

This commit:

* rewrites the components to be up to standard for the content within the libs directory
* Reworks the headers for the payments-next app to be compatible with new localization requirements
* Brings the header up to parity

Closes #FXA-7803
This commit is contained in:
Davey Alvarez 2025-01-28 13:13:46 -08:00 коммит произвёл GitHub
Родитель de6b0bf4b4 93903bda66
Коммит d2511e1973
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
22 изменённых файлов: 587 добавлений и 61 удалений

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

@ -103,6 +103,10 @@ PROFILE_CLIENT_CONFIG__URL=http://localhost:1111
PROFILE_CLIENT_CONFIG__SECRET_BEARER_TOKEN='8675309jenny'
PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
# ImageServer Config
PROFILE_DEFAULT_IMAGES_URL=http://localhost:1111
PROFILE_UPLOADED_IMAGES_URL=http://localhost:1112
# ContentServer Config
CONTENT_SERVER_CLIENT_CONFIG__URL=http://localhost:3030

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

@ -4,8 +4,9 @@
import { getApp } from '@fxa/payments/ui/server';
import { headers } from 'next/headers';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
import { Providers } from '@fxa/payments/ui';
import { Header, Providers } from '@fxa/payments/ui';
import { config } from 'apps/payments/next/config';
import { auth, signOut } from '../../auth';
export default async function RootProviderLayout({
children,
@ -22,6 +23,8 @@ export default async function RootProviderLayout({
const nonce = headers().get('x-nonce') || undefined;
const fetchedMessages = getApp().getFetchedMessages(locale);
const session = await auth();
return (
<Providers
config={{
@ -34,6 +37,15 @@ export default async function RootProviderLayout({
fetchedMessages={fetchedMessages}
nonce={nonce}
>
<Header
auth={{
user: session?.user,
signOut: async () => {
'use server';
await signOut({ redirect: false });
},
}}
/>
{children}
</Providers>
);

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

@ -2,8 +2,6 @@
* 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 Image from 'next/image';
import mozillaLogo from '@fxa/shared/assets/images/moz-logo-bw-rgb.svg';
import './styles/global.css';
// TODO - Replace these placeholders as part of FXA-8227
@ -20,19 +18,6 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<header
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-40 tablet:h-20"
role="banner"
data-testid="header"
>
<Image
src={mozillaLogo}
alt="Mozilla logo"
className="object-contain"
data-testid="branding"
width={120}
/>
</header>
<main className="mt-16 min-h-[calc(100vh_-_4rem)]">{children}</main>
</body>
</html>

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

@ -11,52 +11,54 @@ export default function Index() {
// and instead redirects to the Subscription Management page.
// This page will be fixed before launch by FXA-8304
return (
<div>
<h1 className="text-xxl text-center m-4">Welcome</h1>
<div className="flex-col">
<h2 className="text-xl">With auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/vpn/monthly/landing"
>
Redirect
</Link>
<>
<main className="mt-16 min-h-[calc(100vh_-_4rem)]">
<h1 className="text-xxl text-center m-4">Welcome</h1>
<div className="flex-col">
<h2 className="text-xl">With auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/vpn/monthly/landing"
>
Redirect
</Link>
</div>
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Yearly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123donepro/yearly/landing"
>
Redirect
</Link>
</div>
</div>
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Yearly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123donepro/yearly/landing"
>
Redirect
</Link>
<h2 className="text-xl mt-8">Without auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>123Done Pro - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123donepro/monthly/new"
>
Redirect
</Link>
</div>
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Yearly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123donepro/yearly/new"
>
Redirect
</Link>
</div>
</div>
</div>
<h2 className="text-xl mt-8">Without auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>123Done Pro - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123donepro/monthly/new"
>
Redirect
</Link>
</div>
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Yearly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123donepro/yearly/new"
>
Redirect
</Link>
</div>
</div>
</div>
</div>
</main>
</>
);
}

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

@ -119,6 +119,12 @@ export class PaymentsNextConfig extends NestAppRootConfig {
@IsUrl({ require_tld: false })
paymentsNextHostedUrl!: string;
@IsUrl({ require_tld: false })
profileDefaultImagesUrl!: string;
@IsUrl({ require_tld: false })
profileUploadedImagesUrl!: string;
@IsString()
subscriptionsUnsupportedLocations!: string;

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

@ -20,6 +20,9 @@ export function middleware(request: NextRequest) {
const PAYPAL_SCRIPT_URL = 'https://www.paypal.com';
const PAYPAL_API_URL = process.env.CSP__PAYPAL_API;
const PAYPAL_OBJECTS = 'https://www.paypalobjects.com';
const PROFILE_CLIENT_URL = process.env.PROFILE_CLIENT_CONFIG__URL;
const PROFILE_DEFAULT_IMAGES_URL = process.env.PROFILE_DEFAULT_IMAGES_URL;
const PROFILE_UPLOADED_IMAGES_URL = process.env.PROFILE_UPLOADED_IMAGES_URL;
/*
* CSP Notes
@ -40,7 +43,7 @@ export function middleware(request: NextRequest) {
process.env.NODE_ENV === 'production' ? '' : `'unsafe-eval'`
} https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: ${accountsStaticCdn} ${PAYPAL_OBJECTS};
img-src 'self' blob: data: ${accountsStaticCdn} ${PAYPAL_OBJECTS} ${PROFILE_CLIENT_URL} ${PROFILE_DEFAULT_IMAGES_URL} ${PROFILE_UPLOADED_IMAGES_URL};
font-src 'self';
object-src 'none';
base-uri 'self';

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

@ -8,6 +8,7 @@ export * from './lib/client/components/BaseButton';
export * from './lib/client/components/CheckoutForm';
export * from './lib/client/components/CheckoutCheckbox';
export * from './lib/client/components/CouponForm';
export * from './lib/client/components/Header';
export * from './lib/client/components/PaymentStateObserver';
export * from './lib/client/components/PaymentInputHandler';
export * from './lib/client/components/PaymentSection';

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

@ -0,0 +1,33 @@
# Component - Header
payments-header-help =
.title = Help
.aria-label = Help
.alt = Help
payments-header-bento =
.title = Mozilla products # hover text for bento menu
.aria-label = Mozilla products # hover text for bento menu
.alt = Mozilla Logo # bento menu logo alt text
payments-header-bento-close =
.alt = Close # Icon shown in close-menu button
payments-header-bento-tagline = More products from Mozilla that protect your privacy
payments-header-bento-firefox-desktop = { -brand-firefox } Browser for Desktop
payments-header-bento-firefox-mobile = { -brand-firefox } Browser for Mobile
payments-header-bento-monitor = { -product-mozilla-monitor }
payments-header-bento-firefox-relay = { -product-firefox-relay }
payments-header-bento-vpn = { -product-mozilla-vpn }
payments-header-bento-pocket = { -product-pocket }
payments-header-bento-made-by-mozilla = Made by { -brand-mozilla }
payments-header-avatar =
.title = Mozilla account menu # hover text for avatar dropdown menu
payments-header-avatar-icon =
.alt = Account profile picture # profile picture alt text
payments-header-avatar-expanded-signed-in-as = Signed in as
payments-header-avatar-expanded-sign-out = Sign out

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

@ -0,0 +1,375 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use client';
import Image from 'next/image';
import helpIcon from '@fxa/shared/assets/images/help.svg';
import mozillaIcon from '@fxa/shared/assets/images/moz-logo-bw-rgb.svg';
import bentoIcon from '@fxa/shared/assets/images/bento.svg';
import defaultAvatarIcon from '@fxa/shared/assets/images/avatar-default.svg';
import closeIcon from '@fxa/shared/assets/images/close.svg';
import desktopIcon from '@fxa/shared/assets/images/desktop.svg';
import mobileIcon from '@fxa/shared/assets/images/mobile.svg';
import monitorIcon from '@fxa/shared/assets/images/monitor.svg';
import relayIcon from '@fxa/shared/assets/images/relay.svg';
import vpnIcon from '@fxa/shared/assets/images/vpn-logo.svg';
import pocketIcon from '@fxa/shared/assets/images/pocket.svg';
import signOutIcon from '@fxa/shared/assets/images/sign-out.svg';
import { LinkExternal } from '@fxa/shared/react';
import { Localized } from '@fluent/react';
import { useState } from 'react';
import { useClickOutsideEffect } from '../../hooks/useClickOutsideEffect';
import { useEscKeydownEffect } from '../../hooks/useEscKeydownEffect';
import { User } from 'next-auth';
import { constructHrefWithUtm } from '../../../utils/constructHrefWithUtm';
import { OFFERING_LINKS } from '../../../constants';
import { useParams, useRouter } from 'next/navigation';
type HeaderProps = {
auth?: {
user: User | undefined;
signOut: () => Promise<void>;
};
};
export const Header = ({ auth }: HeaderProps) => {
const router = useRouter();
const { locale, offeringId, interval } = useParams();
const signedIn = auth && auth.user;
const [isBentoMenuRevealed, setBentoMenuRevealed] = useState(false);
const toggleBentoMenuRevealed = () => {
setBentoMenuRevealed(!isBentoMenuRevealed);
setAvatarMenuRevealed(false);
};
const bentoMenuInsideRef =
useClickOutsideEffect<HTMLDivElement>(setBentoMenuRevealed);
const [isAvatarMenuRevealed, setAvatarMenuRevealed] = useState(false);
const toggleAvatarMenuRevealed = () => {
setAvatarMenuRevealed(!isAvatarMenuRevealed);
setBentoMenuRevealed(false);
};
const avatarMenuInsideRef = useClickOutsideEffect<HTMLDivElement>(
setAvatarMenuRevealed
);
useEscKeydownEffect(() => {
setBentoMenuRevealed(false);
setAvatarMenuRevealed(false);
});
const links = {
desktop: constructHrefWithUtm(
OFFERING_LINKS.desktop,
'mozilla-websites',
'moz-subplat',
'bento',
'fx-desktop',
'permanent'
),
mobile: constructHrefWithUtm(
OFFERING_LINKS.mobile,
'mozilla-websites',
'moz-subplat',
'bento',
'fx-mobile',
'permanent'
),
monitor: constructHrefWithUtm(
OFFERING_LINKS.monitor,
'mozilla-websites',
'moz-subplat',
'bento',
'monitor',
'permanent'
),
relay: constructHrefWithUtm(
OFFERING_LINKS.relay,
'mozilla-websites',
'moz-subplat',
'bento',
'relay',
'permanent'
),
vpn: constructHrefWithUtm(
OFFERING_LINKS.vpn,
'mozilla-websites',
'moz-subplat',
'bento',
'vpn',
'permanent'
),
pocket: OFFERING_LINKS.pocket,
};
const iconClassNames = 'inline-block w-5 -mb-1 me-1';
return (
<header
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-40 tablet:h-20"
role="banner"
>
<div className="flex items-center">
<Image
src={mozillaIcon}
alt="Mozilla logo"
className="object-contain"
width={120}
/>
</div>
<div className="flex items-center leading-normal">
<Localized id="payments-header-help">
<LinkExternal
href="https://support.mozilla.org/products/mozilla-account"
title="Help"
className="inline-block relative p-2 -m-2 z-[1] rounded hover:bg-grey-100"
>
<Image
src={helpIcon}
aria-label="Help"
alt="Help"
role="img"
className="w-5 text-violet-900"
/>
</LinkExternal>
</Localized>
{/** Bento Menu, TODO: convert to Radix Primitive as part of FXA-11035 */}
<div
className="relative self-center flex mx-2"
ref={bentoMenuInsideRef}
>
<Localized id="payments-header-bento">
<button
onClick={toggleBentoMenuRevealed}
title="Mozilla products"
aria-label="Mozilla products"
aria-expanded={!!isBentoMenuRevealed}
aria-haspopup="menu"
className="rounded p-2 mx-2 border-transparent hover:bg-grey-100 transition-standard desktop:mx-8"
>
<Image
src={bentoIcon}
alt="Mozilla Logo"
className="w-5 text-violet-900"
/>
</button>
</Localized>
{/** Bento Dropdown */}
{isBentoMenuRevealed && (
<div
className="w-full h-full fixed top-0 start-0 bg-white z-10 desktop:-start-50
mobileLandscape:h-auto mobileLandscape:top-10 mobileLandscape:-start-52 mobileLandscape:w-64
mobileLandscape:bg-white mobileLandscape:absolute mobileLandscape:shadow-md mobileLandscape:rounded-lg"
>
<div className="flex flex-wrap">
<div className="flex w-full py-4 gap-2 items-center flex-col tablet:w-auto tablet:relative">
<button onClick={() => setBentoMenuRevealed(false)}>
<Localized id="payments-header-bento-close">
<Image
src={closeIcon}
alt="Close"
width="16"
height="16"
className="absolute top-5 end-5 mobileLandscape:hidden fill-current"
/>
</Localized>
</button>
<div className="mt-12 px-8 text-center mobileLandscape:mt-0">
<Localized id="payments-header-bento-tagline">
<h2>
More products from Mozilla that protect your privacy
</h2>
</Localized>
</div>
<div className="w-full text-xs">
<ul className="list-none">
<li>
<LinkExternal
href={links.desktop}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
<Image src={desktopIcon} alt="" />
</div>
<Localized id="payments-header-bento-firefox-desktop">
<span>Firefox Browser for Desktop</span>
</Localized>
</LinkExternal>
</li>
<li>
<LinkExternal
href={links.mobile}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
<Image src={mobileIcon} alt="" />
</div>
<Localized id="payments-header-bento-firefox-mobile">
<span>Firefox Browser for Mobile</span>
</Localized>
</LinkExternal>
</li>
<li>
<LinkExternal
href={links.monitor}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
<Image src={monitorIcon} alt="" />
</div>
<Localized id="payments-header-bento-monitor">
<span>Mozilla Monitor</span>
</Localized>
</LinkExternal>
</li>
<li>
<LinkExternal
href={links.relay}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
<Image src={relayIcon} alt="" />
</div>
<Localized id="payments-header-bento-firefox-relay">
<span>Firefox Relay</span>
</Localized>
</LinkExternal>
</li>
<li>
<LinkExternal
href={links.vpn}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
<Image src={vpnIcon} alt="" />
</div>
<Localized id="payments-header-bento-vpn">
<span>Mozilla VPN</span>
</Localized>
</LinkExternal>
</li>
<li>
<LinkExternal
href={links.pocket}
className="block p-2 ps-6 hover:bg-grey-100"
>
<div className={iconClassNames}>
<Image src={pocketIcon} alt="" />
</div>
<Localized id="payments-header-bento-pocket">
<span>Pocket</span>
</Localized>
</LinkExternal>
</li>
</ul>
</div>
<Localized id="payments-header-bento-made-by-mozilla">
<LinkExternal
href="https://www.mozilla.org/"
className="link-blue text-xs underline-offset-4 w-full text-center p-2 block hover:bg-grey-100"
>
Made by Mozilla
</LinkExternal>
</Localized>
</div>
</div>
</div>
)}
</div>
{/** Avatar Menu, TODO: convert to Radix Primitive as part of FXA-11035 */}
{signedIn && (
<div className="relative" ref={avatarMenuInsideRef}>
<Localized id="payments-header-avatar">
<button
onClick={toggleAvatarMenuRevealed}
title="Mozilla account menu"
>
<Localized id="payments-header-avatar-icon">
<Image
unoptimized={true}
src={auth.user?.image ?? defaultAvatarIcon}
alt="Account profile picture"
width="10"
height="10"
className="w-10 rounded-full"
/>
</Localized>
</button>
</Localized>
{/** Avatar Dropdown */}
{isAvatarMenuRevealed && (
<div
dir="ltr"
className="w-64 bg-white absolute shadow-md rounded-lg ltr:-left-52"
role="menu"
>
<div className="flex flex-wrap">
<div className="flex w-full p-4 items-center">
<div className="ltr:mr-3 flex-none">
<Localized id="payments-header-avatar-icon">
<Image
unoptimized={true}
src={auth.user?.image ?? defaultAvatarIcon}
alt="Account profile picture"
width="10"
height="10"
className="w-10 rounded-full"
/>
</Localized>
</div>
<p className="leading-5 max-w-full truncate">
<Localized id="payments-header-avatar-expanded-signed-in-as">
<span className="text-grey-400 text-xs">
Signed in as
</span>
</Localized>
<span className="font-bold block truncate">
{/* eslint-disable @typescript-eslint/no-non-null-assertion */}
{auth.user!.name || auth.user!.email}
</span>
</p>
</div>
<div className="w-full">
<div className="bg-gradient-to-r from-blue-500 via-pink-700 to-yellow-500 h-px" />
<div className="px-4 py-5">
<button
onClick={async () => {
// As of this commit, both router.push and direct location.href overwriting are necessary to ensure
// the browser is in the correct state without any contentless flashes
await auth.signOut();
const redirectUrl = `/${locale}/${offeringId}/${interval}/new`;
router.push(redirectUrl);
window.location.href = redirectUrl;
}}
className="pl-3 group"
>
<Image
src={signOutIcon}
alt=""
height="18"
width="18"
className="ltr:mr-3 inline-block stroke-current align-middle transform"
/>
<Localized id="payments-header-avatar-expanded-sign-out">
<span className="group-hover:underline">
Sign out
</span>
</Localized>
</button>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
</header>
);
};

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

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { useEffect, useRef } from 'react';
export function useClickOutsideEffect<T>(
onClickOutside: (...args: any[]) => void
) {
const insideEl = useRef<T>(null);
useEffect(() => {
const onBodyClick = (ev: MouseEvent) => {
if (
insideEl.current instanceof HTMLElement &&
ev.target instanceof HTMLElement &&
!insideEl.current.contains(ev.target)
) {
onClickOutside();
}
};
document.body.addEventListener('click', onBodyClick);
return () => document.body?.removeEventListener('click', onBodyClick);
}, [onClickOutside]);
return insideEl;
}

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

@ -0,0 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { useEffect } from 'react';
export function useEscKeydownEffect(onEscKeydown: (...args: any[]) => any) {
useEffect(() => {
const handler = ({ key }: KeyboardEvent) => {
if (key === 'Escape') {
onEscKeydown();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [onEscKeydown]);
}

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

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export const OFFERING_LINKS = {
desktop: 'https://www.mozilla.org/firefox/new/',
mobile: 'https://www.mozilla.org/firefox/mobile/',
monitor: 'https://monitor.mozilla.org/',
relay: 'https://relay.firefox.com/',
vpn: 'https://vpn.mozilla.org/',
pocket:
'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',
};

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

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* 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' | 'moz-subplat',
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}`;
};

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

@ -0,0 +1 @@
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M50.807 44.827c-4.1 6.035-10.958 10.03-18.807 10.03-7.85 0-14.71-3.995-18.811-10.03-1.175-1.728-1.061-4.032.31-5.613 4.832-5.577 11.539 1.929 18.501 1.929 6.962 0 13.664-7.506 18.5-1.93 1.367 1.582 1.482 3.886.307 5.614zM32 9.143c7.57 0 13.714 6.14 13.714 13.714 0 7.58-6.144 13.714-13.714 13.714-7.575 0-13.714-6.134-13.714-13.714 0-7.575 6.14-13.714 13.714-13.714zM32 0C14.327 0 0 14.327 0 32c0 17.678 14.327 32 32 32 17.673 0 32-14.322 32-32C64 14.327 49.673 0 32 0z" fill="url(#paint0_linear)"/><defs><linearGradient id="paint0_linear" x1="32" y1="96" x2="96" y2="32" gradientUnits="userSpaceOnUse"><stop stop-color="#0090ED"/><stop offset=".356" stop-color="#5B6DF8"/><stop offset=".636" stop-color="#9059FF"/><stop offset="1" stop-color="#B833E1"/></linearGradient></defs></svg>

После

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

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

@ -0,0 +1,2 @@
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><rect width="6" height="6" rx="1"/><rect x="9" width="6" height="6" rx="1"/><rect x="18" width="6" height="6" rx="1"/><rect y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/><rect x="18" y="9" width="6" height="6" rx="1"/><rect y="18" width="6" height="6" rx="1"/><rect x="9" y="18" width="6" height="6" rx="1"/><rect x="18" y="18" width="6" height="6" rx="1"/>
</svg>

После

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

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

После

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

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

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.9999 22.909C18.0248 22.909 22.909 18.0248 22.909 11.9999C22.909 5.97499 18.0248 1.09082 11.9999 1.09082C5.97499 1.09082 1.09082 5.97499 1.09082 11.9999C1.09082 18.0248 5.97499 22.909 11.9999 22.909Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.82544 8.72704C9.35501 7.22164 10.8931 6.31767 12.466 6.58745C14.0389 6.85724 15.1878 8.22212 15.1854 9.81795C15.1854 11.9998 11.9127 13.0907 11.9127 13.0907" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path fill-rule="evenodd" clip-rule="evenodd" d="M11.9998 18.5456C12.6023 18.5456 13.0908 18.0572 13.0908 17.4547C13.0908 16.8522 12.6023 16.3638 11.9998 16.3638C11.3974 16.3638 10.9089 16.8522 10.9089 17.4547C10.9089 18.0572 11.3974 18.5456 11.9998 18.5456Z" fill="currentColor"/></svg>

После

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

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

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 24 24" preserveAspectRatio="slice" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(5.000000, 1.000000)" stroke="#000000" stroke-width="2">
<rect x="0" y="0" width="14" height="22" rx="2"></rect>
<path d="M0,4 L14,4"></path>
<path d="M0,18 L14,18"></path>
</g>
</g>
</svg>

После

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

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

После

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

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

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 77.2 79.73"><defs><linearGradient id="a" x1="460.66" y1="-120.15" x2="449.39" y2="-109.49" gradientTransform="matrix(1 0 0 -1 -389 -47.01)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#054096" stop-opacity=".5"/><stop offset=".1" stop-color="#173ba1" stop-opacity=".44"/><stop offset=".29" stop-color="#3434b3" stop-opacity=".33"/><stop offset=".49" stop-color="#482ec1" stop-opacity=".22"/><stop offset=".68" stop-color="#552bc8" stop-opacity=".11"/><stop offset=".86" stop-color="#592acb" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="3.39" y1="42.56" x2="73.6" y2="42.56" gradientTransform="matrix(1 0 0 -1 0 82.99)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9059ff"/><stop offset="1" stop-color="#f770ff"/></linearGradient><linearGradient id="c" x1="40.92" y1="20.28" x2="81.59" y2="3.07" gradientTransform="matrix(1 0 0 -1 0 82.99)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#54ffbd"/><stop offset="1" stop-color="#0df"/></linearGradient></defs><path d="M70.19 18.69l-28.4-16a6.81 6.81 0 00-6.5 0l-28.5 16a6.68 6.68 0 00-3.4 5.8v31.9a6.56 6.56 0 003.4 5.8l28.4 15.9a6.29 6.29 0 003.3.9 6.56 6.56 0 003.3-.9l28.4-15.9a6.68 6.68 0 003.4-5.8v-31.9a6.38 6.38 0 00-3.4-5.8z" transform="translate(-3.39 -1.87)" fill="url(#b)"/><path d="M15.3 46.92l-1.7 1a2.62 2.62 0 00-1 3.6 2.65 2.65 0 002.3 1.4 3.08 3.08 0 001.3-.3l1.7-1a2.62 2.62 0 001-3.6 2.57 2.57 0 00-3.6-1.1zm10.4-5.9l-3.5 2a2.62 2.62 0 00-1 3.6 2.65 2.65 0 002.3 1.4 3.08 3.08 0 001.3-.3l3.5-2a2.62 2.62 0 001-3.6 2.57 2.57 0 00-3.6-1.1zm14.1-4.89a2.36 2.36 0 00-1.4-1.2V14.82a2.69 2.69 0 00-2.7-2.7 2.61 2.61 0 00-2.6 2.7v20.2a2.36 2.36 0 00-1.4 1.2 2.66 2.66 0 00-.1 2.3 2.66 2.66 0 00.1 2.3 2.36 2.36 0 001.4 1.2v20.2a2.7 2.7 0 005.4 0v-20.3a2.36 2.36 0 001.4-1.2 2.66 2.66 0 00.1-2.3 3.08 3.08 0 00-.2-2.29zm6.81-6.91l-3.5 2a2.62 2.62 0 00-1 3.6 2.65 2.65 0 002.3 1.4 3.08 3.08 0 001.3-.3l3.5-2a2.62 2.62 0 001-3.6 2.57 2.57 0 00-3.6-1.1zm8.29 1a3.08 3.08 0 001.3-.3l1.7-1a2.642 2.642 0 10-2.6-4.6l-1.7 1a2.62 2.62 0 00-1 3.6 2.56 2.56 0 002.3 1.3zm3 17.7l-1.7-1a2.642 2.642 0 00-2.6 4.6l1.7 1a3.08 3.08 0 001.3.3 2.65 2.65 0 002.3-1.4 2.47 2.47 0 00-1-3.5zm-8.7-4.9l-3.5-2a2.642 2.642 0 10-2.6 4.6l3.5 2a3.08 3.08 0 001.3.3 2.65 2.65 0 002.3-1.4 2.45 2.45 0 00-1-3.5zm-27-9.2l3.5 2a3.08 3.08 0 001.3.3 2.65 2.65 0 002.3-1.4 2.62 2.62 0 00-1-3.6l-3.5-2a2.62 2.62 0 00-3.6 1 2.8 2.8 0 001 3.7zm-4.3-8.5l-1.7-1a2.642 2.642 0 10-2.6 4.6l1.7 1a3.08 3.08 0 001.3.3 2.65 2.65 0 002.3-1.4 2.53 2.53 0 00-1-3.5z" fill="#20133a"/></svg>

После

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

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

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M6.333 9.26H17M12.733 4.993L17 9.26l-4.267 4.267M9.533 16.727H2.067A1.067 1.067 0 011 15.66V2.86c0-.589.478-1.067 1.067-1.067h7.466" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="black"/></svg>

После

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

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

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g transform="scale(.2)"><path fill-rule="evenodd" clip-rule="evenodd" d="M40 9a6 6 0 100 12 6 6 0 000-12zm-14 6c0-7.732 6.268-14 14-14s14 6.268 14 14-6.268 14-14 14c-2.411 0-4.68-.61-6.66-1.683l-6.023 6.023c.455.838.826 1.729 1.103 2.66h23.16C53.3 30.217 58.658 26 65 26c7.732 0 14 6.268 14 14s-6.268 14-14 14c-2.411 0-4.68-.61-6.66-1.683l-6.023 6.023A13.938 13.938 0 0154 65c0 7.732-6.268 14-14 14s-14-6.268-14-14 6.268-14 14-14c2.411 0 4.68.61 6.66 1.683l6.023-6.023A13.91 13.91 0 0151.58 44H28.42C26.7 49.783 21.342 54 15 54 7.268 54 1 47.732 1 40s6.268-14 14-14c2.411 0 4.68.61 6.66 1.683l6.023-6.023A13.939 13.939 0 0126 15zm8 50a6 6 0 1112 0 6 6 0 01-12 0zM15 34a6 6 0 100 12 6 6 0 000-12zm44 6a6 6 0 1112 0 6 6 0 01-12 0z" fill="#20123a"/></g></svg>

После

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