Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/fxa-settings/src/components/DeviceInfoBlock/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ device-info-block-location-unknown = Location unknown
# Variable { $browserName } is the browser that created the request (e.g., Firefox)
# Variable { $genericOSName } is the name of the operating system that created the request (e.g., MacOS, Windows, iOS)
device-info-browser-os = { $browserName } on { $genericOSName }
# Variable { $browserName } is the browser that created the request (e.g., Firefox)
# Variable { $deviceName } is the user-chosen name of the device that created the request (e.g., Laurel's MacBook Pro)
device-info-browser-device = { $browserName } on { $deviceName }
# Variable { $ipAddress } represents the IP address where the request originated
# The IP address is a string of numbers separated by periods (e.g., 192.158.1.38)
device-info-ip-address = IP address: { $ipAddress }
Original file line number Diff line number Diff line change
Expand Up @@ -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 DeviceInfoBlock from '.';
import DeviceInfoBlock, { DeviceNameDisplay } from '.';
import { RemoteMetadata } from '../../lib/types';
import AppLayout from '../AppLayout';
import { Meta } from '@storybook/react';
Expand All @@ -21,12 +21,19 @@ export default {
decorators: [withLocalization],
} as Meta;

const storyWithProps = (props?: Partial<RemoteMetadata>) => {
// `metadata` merges into the mock rather than spreading onto the component —
// spreading `Partial<RemoteMetadata>` as JSX props type-checks (TS skips
// excess-property checks on spreads) but silently renders nothing, which is
// what `WithDeviceName` used to do.
const storyWithProps = (
metadata?: Partial<RemoteMetadata>,
deviceNameDisplay?: DeviceNameDisplay
) => {
const story = () => (
<AppLayout>
<DeviceInfoBlock
remoteMetadata={MOCK_METADATA_UNKNOWN_LOCATION}
{...props}
remoteMetadata={{ ...MOCK_METADATA_UNKNOWN_LOCATION, ...metadata }}
{...{ deviceNameDisplay }}
/>
</AppLayout>
);
Expand Down Expand Up @@ -56,3 +63,15 @@ export const WithFullLocation = storyWithProps({
region: MOCK_REGION,
country: MOCK_COUNTRY,
});

// How the Pair2 mobile cards render it: no heading, device name in the
// browser line.
export const WithInlineDeviceName = storyWithProps(
{ deviceName: MOCK_DEVICE_NAME },
'inline'
);

export const WithHiddenDeviceName = storyWithProps(
{ deviceName: MOCK_DEVICE_NAME },
'hidden'
);
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,61 @@ describe('DeviceInfoBlock component', () => {

screen.getByText('Vancouver, British Columbia, Canada (estimated)');
});

describe('deviceNameDisplay', () => {
it('folds the device name into the browser line when set to inline', () => {
renderWithLocalizationProvider(
<DeviceInfoBlock
remoteMetadata={MOCK_METADATA_WITH_DEVICE_NAME}
deviceNameDisplay="inline"
/>
);

screen.getByText('Firefox on Ultron');
expect(
screen.queryByRole('heading', { level: 2 })
).not.toBeInTheDocument();
expect(screen.queryByText('Firefox on macOS')).not.toBeInTheDocument();
});

it('omits the device name entirely when set to hidden', () => {
renderWithLocalizationProvider(
<DeviceInfoBlock
remoteMetadata={MOCK_METADATA_WITH_DEVICE_NAME}
deviceNameDisplay="hidden"
/>
);

screen.getByText('Firefox on macOS');
expect(
screen.queryByRole('heading', { level: 2 })
).not.toBeInTheDocument();
expect(screen.queryByText('Firefox on Ultron')).not.toBeInTheDocument();
});

// `pairing-authority-integration` leaves `deviceName` unset rather than
// substituting a generic type, so `inline` has to degrade to the OS line.
it('falls back to the OS line under inline when there is no device name', () => {
renderWithLocalizationProvider(
<DeviceInfoBlock
remoteMetadata={MOCK_METADATA_WITH_LOCATION}
deviceNameDisplay="inline"
/>
);

screen.getByText('Firefox on macOS');
});
});

it('replaces the default wrapper classes when className is given', () => {
const { container } = renderWithLocalizationProvider(
<DeviceInfoBlock
remoteMetadata={MOCK_METADATA_WITH_LOCATION}
className="rounded-md border"
/>
);

expect(container.firstElementChild).toHaveClass('rounded-md', 'border');
expect(container.firstElementChild).not.toHaveClass('mt-8', 'mb-4');
});
});
60 changes: 50 additions & 10 deletions packages/fxa-settings/src/components/DeviceInfoBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,40 @@
import { FtlMsg } from 'fxa-react/lib/utils';
import { RemoteMetadata } from '../../lib/types';

/**
* How the device name is presented, when the pairing payload carries one:
*
* - `heading` — its own `<h2>` above the browser line, which names the OS.
* What the live `Pair/*` pages render.
* - `inline` — folded into the browser line ("Firefox on Laurel's MacBook
* Pro"), with no heading. What the Pair2 mobile cards render.
* - `hidden` — omitted entirely; the browser line names the OS.
*
* When the payload has no device name, all three render the OS line — see
* `pairing-authority-integration`, which deliberately leaves `deviceName`
* unset rather than substituting a generic type like "desktop".
*/
export type DeviceNameDisplay = 'heading' | 'inline' | 'hidden';

// Remote metadata is obtained from pairing channel
// Some of this data may align with the account.ts model but the keys are slightly different (e.g., `state` vs `region`)
type DeviceInfoBlockProps = { remoteMetadata: RemoteMetadata };
type DeviceInfoBlockProps = {
remoteMetadata: RemoteMetadata;
/**
* Replaces the default wrapper classes, for callers that present the block
* differently — the Pair2 cards render it as a bordered box. The per-line
* `text-xs` stays put, so overriding this only moves the box, it does not
* resize the copy.
*/
className?: string;
deviceNameDisplay?: DeviceNameDisplay;
};

export const DeviceInfoBlock = ({ remoteMetadata }: DeviceInfoBlockProps) => {
export const DeviceInfoBlock = ({
remoteMetadata,
className = 'mt-8 mb-4',
deviceNameDisplay = 'heading',
}: DeviceInfoBlockProps) => {
const {
deviceName,
deviceFamily,
Expand Down Expand Up @@ -58,14 +87,25 @@ export const DeviceInfoBlock = ({ remoteMetadata }: DeviceInfoBlockProps) => {
};

return (
<div className="mt-8 mb-4">
{deviceName && <h2 className="mb-4 text-base">{deviceName}</h2>}
<FtlMsg
id="device-info-browser-os"
vars={{ browserName: deviceFamily, genericOSName: deviceOS }}
>
<p className="text-xs">{`${deviceFamily} on ${deviceOS}`}</p>
</FtlMsg>
<div {...{ className }}>
{deviceNameDisplay === 'heading' && deviceName && (
<h2 className="mb-4 text-base">{deviceName}</h2>
)}
{deviceNameDisplay === 'inline' && deviceName ? (
<FtlMsg
id="device-info-browser-device"
vars={{ browserName: deviceFamily, deviceName }}
>
<p className="text-xs">{`${deviceFamily} on ${deviceName}`}</p>
</FtlMsg>
) : (
<FtlMsg
id="device-info-browser-os"
vars={{ browserName: deviceFamily, genericOSName: deviceOS }}
>
<p className="text-xs">{`${deviceFamily} on ${deviceOS}`}</p>
</FtlMsg>
)}
<p className="text-xs">
<LocalizedLocation />
</p>
Expand Down
5 changes: 5 additions & 0 deletions packages/fxa-settings/src/components/images/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ confetti-falling-image-aria-label =
# In this context, “VPN” is a VPN service built into the Firefox browser, and generally isn't localized differently than “VPN”
vpn-welcome-image-aria-label =
.aria-label = { -brand-firefox } window with a circular badge showing a green checkmark and “VPN,” showing the VPN is active.
sync-devices-image-aria-label =
.aria-label = A desktop browser window and a mobile phone, both syncing, with the { -brand-firefox } mascot alongside them
# Aria label for the Firefox logo and wordmark shown together as a brand lockup
firefox-wordmark-image-aria-label =
.aria-label = { -brand-firefox }
Loading
Loading