Skip to content

Commit 5b02a8b

Browse files
fix(settings): group connected services by deviceId to avoid name merging
1 parent 30e2ded commit 5b02a8b

2 files changed

Lines changed: 26 additions & 111 deletions

File tree

packages/fxa-settings/src/components/Settings/ConnectedServices/index.test.tsx

Lines changed: 9 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@
55
import React from 'react';
66
import { act, fireEvent, screen } from '@testing-library/react';
77
import ConnectedServices, { sortAndFilterConnectedClients } from '.';
8-
import {
9-
Account,
10-
AlertBarInfo,
11-
AppContext,
12-
OAuthNativeClients,
13-
} from '../../../models';
8+
import { Account, AlertBarInfo, AppContext } from '../../../models';
149
import {
1510
renderWithRouter,
1611
mockAppContext,
@@ -158,10 +153,13 @@ describe('Connected Services', () => {
158153
expect(result[result.length - 1]).toHaveTextContent('6 months ago');
159154
});
160155

161-
const { sortedAndUniqueClients, groupedByName } =
162-
sortAndFilterConnectedClients(MOCK_SERVICES);
156+
const { sortedAndUniqueClients } = sortAndFilterConnectedClients(MOCK_SERVICES);
157+
158+
const monitorClients = MOCK_SERVICES.filter(
159+
(item) => item.name === 'Mozilla Monitor'
160+
);
163161

164-
expect(sortedAndUniqueClients.length).toEqual(14);
162+
expect(sortedAndUniqueClients.length).toEqual(13);
165163

166164
expect(
167165
sortedAndUniqueClients.filter((item) => item.name === 'Mozilla Monitor')
@@ -172,7 +170,7 @@ describe('Connected Services', () => {
172170
(item) => item.name === 'Mozilla Monitor'
173171
)[0].lastAccessTime
174172
).toEqual(1570736983000);
175-
expect(groupedByName['Mozilla Monitor'].length).toEqual(2);
173+
expect(monitorClients.length).toEqual(2);
176174
});
177175

178176
it('should show the monitor icon and link', async () => {
@@ -314,7 +312,7 @@ describe('Connected Services', () => {
314312
);
315313
expect(
316314
await screen.findAllByTestId('connected-service-sign-out')
317-
).toHaveLength(14);
315+
).toHaveLength(13);
318316
});
319317

320318
it('renders proper modal when "sign out" is clicked', async () => {
@@ -608,85 +606,4 @@ describe('Connected Services', () => {
608606
expect(mockWindowAssign).toHaveBeenCalledWith('foo-bar/signin');
609607
});
610608
});
611-
612-
describe('scope-based sub row', () => {
613-
const baseMockClient = {
614-
deviceId: null,
615-
sessionTokenId: null,
616-
refreshTokenId: 'abc123',
617-
isCurrentSession: false,
618-
deviceType: null,
619-
createdTime: 1571412069000,
620-
lastAccessTime: 1571412069000,
621-
location: { city: null, country: null, state: null, stateCode: null },
622-
userAgent: '',
623-
os: null,
624-
createdTimeFormatted: 'a month ago',
625-
lastAccessTimeFormatted: 'a month ago',
626-
approximateLastAccessTime: null,
627-
approximateLastAccessTimeFormatted: null,
628-
};
629-
630-
const renderWithClient = (client: Record<string, unknown>) => {
631-
const account = {
632-
attachedClients: [client],
633-
disconnectClient: jest.fn().mockResolvedValue(true),
634-
} as unknown as Account;
635-
renderWithRouter(
636-
<AppContext.Provider value={mockAppContext({ account })}>
637-
<ConnectedServices />
638-
</AppContext.Provider>
639-
);
640-
};
641-
642-
it('does not render scope sub-entry when scope is null and client ID is OAuthNative', async () => {
643-
renderWithClient({
644-
...baseMockClient,
645-
clientId: OAuthNativeClients.FirefoxDesktop,
646-
name: 'Firefox Desktop',
647-
scope: null,
648-
});
649-
await screen.findByTestId('settings-connected-service');
650-
expect(screen.queryByTestId('scope-service')).not.toBeInTheDocument();
651-
});
652-
653-
it('does not render scope sub-entry when scope has oldsync and profile but no relay', async () => {
654-
renderWithClient({
655-
...baseMockClient,
656-
clientId: OAuthNativeClients.FirefoxDesktop,
657-
name: 'Firefox Desktop',
658-
scope: ['https://identity.mozilla.com/apps/oldsync', 'profile'],
659-
});
660-
await screen.findByTestId('settings-connected-service');
661-
expect(screen.queryByTestId('scope-service')).not.toBeInTheDocument();
662-
});
663-
664-
it('renders Relay scope sub-entry when scope includes relay and client ID is OAuthNative', async () => {
665-
renderWithClient({
666-
...baseMockClient,
667-
clientId: OAuthNativeClients.FirefoxIOS,
668-
name: 'Firefox for iOS',
669-
scope: [
670-
'https://identity.mozilla.com/apps/oldsync',
671-
'profile',
672-
'https://identity.mozilla.com/apps/relay',
673-
],
674-
});
675-
await screen.findByTestId('settings-connected-service');
676-
const scopeEntry = screen.getByTestId('scope-service');
677-
expect(scopeEntry).toBeInTheDocument();
678-
expect(scopeEntry).toHaveTextContent('Firefox Relay');
679-
});
680-
681-
it('does not render scope sub-entry when scope includes relay but client ID is not OAuthNative', async () => {
682-
renderWithClient({
683-
...baseMockClient,
684-
clientId: '9ebfe2c2f9ea3c58',
685-
name: 'Firefox Relay',
686-
scope: ['profile', 'https://identity.mozilla.com/apps/relay'],
687-
});
688-
await screen.findByTestId('settings-connected-service');
689-
expect(screen.queryByTestId('scope-service')).not.toBeInTheDocument();
690-
});
691-
});
692609
});

packages/fxa-settings/src/components/Settings/ConnectedServices/index.tsx

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
import React from 'react';
6+
import { ApolloError } from '@apollo/client';
57
import { Localized, useLocalization } from '@fluent/react';
68
import { LinkExternal } from 'fxa-react/components/LinkExternal';
79
import { useBooleanState } from 'fxa-react/lib/hooks';
810
import groupBy from 'lodash.groupby';
911
import { forwardRef, useCallback, useState } from 'react';
10-
import { clearSignedInAccountUid, setSigningOut } from '../../../lib/cache';
12+
import { clearSignedInAccountUid } from '../../../lib/cache';
1113
import { logViewEvent } from '../../../lib/metrics';
1214
import { isMobileDevice } from '../../../lib/utilities';
1315
import { AttachedClient, useAccount, useAlertBar } from '../../../models';
@@ -27,12 +29,12 @@ const DEVICES_SUPPORT_URL =
2729
export function sortAndFilterConnectedClients(
2830
attachedClients: Array<AttachedClient>
2931
) {
30-
const groupedByName = groupBy(attachedClients, 'name');
32+
const groupedByDevice = groupBy(attachedClients, (c) => c.deviceId || c.name);
3133

32-
// get a unique (by name) list and sort by time last accessed
33-
const sortedAndUniqueClients = Object.keys(groupedByName)
34+
// get a unique (by device or name) list and sort by time last accessed
35+
const sortedAndUniqueClients = Object.keys(groupedByDevice)
3436
.map((key) => {
35-
return groupedByName[key].sort(
37+
return groupedByDevice[key].sort(
3638
(a: AttachedClient, b: AttachedClient) =>
3739
b.lastAccessTime - a.lastAccessTime
3840
)[0];
@@ -47,14 +49,14 @@ export function sortAndFilterConnectedClients(
4749
}
4850
});
4951

50-
return { groupedByName, sortedAndUniqueClients };
52+
return { groupedByDevice, sortedAndUniqueClients };
5153
}
5254

5355
export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
5456
const alertBar = useAlertBar();
5557
const account = useAccount();
5658
const attachedClients = account.attachedClients;
57-
const { groupedByName, sortedAndUniqueClients } =
59+
const { groupedByDevice, sortedAndUniqueClients } =
5860
sortAndFilterConnectedClients([...attachedClients]);
5961

6062
const showMobilePromo = !sortedAndUniqueClients.filter(isMobileDevice).length;
@@ -82,7 +84,7 @@ export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
8284
const [isRefreshingClients, setIsRefreshingClients] = useState(false);
8385

8486
const clearDisconnectingState = useCallback(
85-
(errorMessage?: string, error?: Error) => {
87+
(errorMessage?: string, error?: ApolloError | Error) => {
8688
hideConfirmDisconnectModal();
8789
setSelectedClient(null);
8890
setReason('');
@@ -105,14 +107,12 @@ export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
105107
// disconnect all clients/sessions with this name since only unique names
106108
// are displayed to the user. This is batched into one network request
107109
// via BatchHttpLink
108-
const groupByKey = client.name ?? 'undefined';
109-
const clientsWithMatchingName = groupedByName[groupByKey];
110-
const hasMultipleSessions = clientsWithMatchingName.length > 1;
110+
const groupByKey = (client.deviceId || client.name) ?? 'undefined';
111+
const sessionsInGroup = groupedByDevice[groupByKey];
112+
const hasMultipleSessions = sessionsInGroup.length > 1;
111113
if (hasMultipleSessions) {
112114
await Promise.all(
113-
clientsWithMatchingName.map(
114-
async (c) => await account.disconnectClient(c)
115-
)
115+
sessionsInGroup.map(async (c) => await account.disconnectClient(c))
116116
);
117117
} else {
118118
await account.disconnectClient(client);
@@ -123,9 +123,8 @@ export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
123123
if (
124124
client.isCurrentSession ||
125125
(hasMultipleSessions &&
126-
clientsWithMatchingName.find((c) => c.isCurrentSession))
126+
sessionsInGroup.find((c) => c.isCurrentSession))
127127
) {
128-
setSigningOut(true);
129128
clearSignedInAccountUid();
130129
window.location.assign(`${window.location.origin}/signin`);
131130
} else if (reason === 'suspicious' || reason === 'lost') {
@@ -150,7 +149,7 @@ export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
150149
[
151150
account,
152151
hideConfirmDisconnectModal,
153-
groupedByName,
152+
groupedByDevice,
154153
revealAdviceModal,
155154
alertBar,
156155
l10n,
@@ -228,7 +227,7 @@ export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
228227
</div>
229228

230229
{!!sortedAndUniqueClients.length &&
231-
sortedAndUniqueClients.map((client) => (
230+
sortedAndUniqueClients.map((client, i) => (
232231
<Service
233232
key={`${client.lastAccessTime}:${client.name || 'unknown'}`}
234233
{...{
@@ -238,7 +237,6 @@ export const ConnectedServices = forwardRef<HTMLDivElement>((_, ref) => {
238237
lastAccessTimeFormatted: client.lastAccessTimeFormatted,
239238
isCurrentSession: client.isCurrentSession,
240239
clientId: client.clientId,
241-
scope: client.scope,
242240
handleSignOut: () => {
243241
onSignOutClick(client);
244242
},

0 commit comments

Comments
 (0)