From 9c69d10fb5a501066d10420f83cd90c385630922 Mon Sep 17 00:00:00 2001 From: Vijay Budhram Date: Thu, 14 May 2026 18:01:44 +0000 Subject: [PATCH] fix(settings): persist pair supplicant device info across refresh Because: refreshing the /pair/auth/complete page caused the supplicant browser and device to render as "Unknown". The WebChannel request for supplicant metadata fails after the pairing session is complete, and the resulting error was silently swallowed. This commit: caches the fetched RemoteMetadata in sessionStorage so a refresh of the "Device connected" page restores the correct values without depending on the now-closed WebChannel session. Fixes FXA-13474 --- .../pages/Pair/AuthComplete/index.test.tsx | 45 ++++++++++++++++++- .../src/pages/Pair/AuthComplete/index.tsx | 25 ++++++++--- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/packages/fxa-settings/src/pages/Pair/AuthComplete/index.test.tsx b/packages/fxa-settings/src/pages/Pair/AuthComplete/index.test.tsx index 1b1729a96c6..124d06a6031 100644 --- a/packages/fxa-settings/src/pages/Pair/AuthComplete/index.test.tsx +++ b/packages/fxa-settings/src/pages/Pair/AuthComplete/index.test.tsx @@ -3,9 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; -import AuthComplete, { viewName } from '.'; +import AuthComplete, { PAIR_DEVICE_INFO_KEY, viewName } from '.'; import { MOCK_METADATA_UNKNOWN_LOCATION } from '../../../components/DeviceInfoBlock/mocks'; import { usePageViewEvent } from '../../../lib/metrics'; import { REACT_ENTRYPOINT } from '../../../constants'; @@ -86,6 +86,7 @@ describe('AuthComplete page', () => { ); mockIntegration = new PAI(); mockIntegration.data = { entrypoint: undefined }; + sessionStorage.clear(); }); it('calls complete() on mount and destroy() on unmount', () => { @@ -96,6 +97,46 @@ describe('AuthComplete page', () => { unmount(); expect(mockIntegration.destroy).toHaveBeenCalled(); }); + + it('restores device info from sessionStorage after refresh', () => { + const cachedInfo = { + deviceFamily: 'Firefox', + deviceOS: 'Windows', + ipAddress: '1.2.3.4', + }; + sessionStorage.setItem(PAIR_DEVICE_INFO_KEY, JSON.stringify(cachedInfo)); + + renderWithLocalizationProvider( + + ); + + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( + 'You are now syncing with: Firefox on Windows' + ); + expect(mockIntegration.getSupplicantMetadata).not.toHaveBeenCalled(); + }); + + it('persists fetched device info to sessionStorage', async () => { + const fetchedInfo = { + deviceFamily: 'Firefox', + deviceOS: 'Android', + ipAddress: '1.2.3.4', + }; + mockIntegration.getSupplicantMetadata.mockResolvedValue(fetchedInfo); + + renderWithLocalizationProvider( + + ); + + await waitFor(() => + expect(sessionStorage.getItem(PAIR_DEVICE_INFO_KEY)).toBe( + JSON.stringify(fetchedInfo) + ) + ); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( + 'You are now syncing with: Firefox on Android' + ); + }); }); describe('Send Tab variant', () => { diff --git a/packages/fxa-settings/src/pages/Pair/AuthComplete/index.tsx b/packages/fxa-settings/src/pages/Pair/AuthComplete/index.tsx index 26d730f36d4..443cf2d26f8 100644 --- a/packages/fxa-settings/src/pages/Pair/AuthComplete/index.tsx +++ b/packages/fxa-settings/src/pages/Pair/AuthComplete/index.tsx @@ -19,6 +19,8 @@ import { isSendTabEntrypoint } from '../../../lib/utilities'; export const viewName = 'pair.auth.complete'; +export const PAIR_DEVICE_INFO_KEY = 'pair.supp.device-info'; + type AuthCompleteProps = { suppDeviceInfo?: RemoteMetadata; supportsFirefoxView?: boolean; @@ -37,7 +39,15 @@ const AuthComplete = ({ usePageViewEvent(viewName, REACT_ENTRYPOINT); const ftlMsgResolver = useFtlMsgResolver(); const [deviceInfo, setDeviceInfo] = useState( - suppDeviceInfoProp + () => { + if (suppDeviceInfoProp) return suppDeviceInfoProp; + try { + const cached = sessionStorage.getItem(PAIR_DEVICE_INFO_KEY); + return cached ? (JSON.parse(cached) as RemoteMetadata) : undefined; + } catch { + return undefined; + } + } ); const authorityIntegration = @@ -49,16 +59,21 @@ const AuthComplete = ({ const deviceOS = deviceInfo?.deviceOS || 'Unknown'; const isSendTab = isSendTabEntrypoint(integration?.data.entrypoint); - // Fetch supplicant metadata if not provided via props + // Fetch supplicant metadata if not already available from props or sessionStorage useEffect(() => { - if (suppDeviceInfoProp) { + if (suppDeviceInfoProp || deviceInfo) { return; } authorityIntegration ?.getSupplicantMetadata() - .then(setDeviceInfo) + .then((info) => { + try { + sessionStorage.setItem(PAIR_DEVICE_INFO_KEY, JSON.stringify(info)); + } catch {} + setDeviceInfo(info); + }) .catch(() => {}); - }, [authorityIntegration, suppDeviceInfoProp]); + }, [authorityIntegration, suppDeviceInfoProp, deviceInfo]); // Signal pairing complete to Firefox on mount useEffect(() => {