Skip to content

Commit d287cea

Browse files
Merge pull request #19911 from mozilla/FXA-12800
feat(signin): Allow cached signin when keys optional
2 parents a5fb588 + 0b14e57 commit d287cea

5 files changed

Lines changed: 127 additions & 18 deletions

File tree

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,19 @@ it('renders the guard when unverified', async () => {
6464
async () =>
6565
await renderWithRouter(
6666
<AppContext.Provider
67-
value={mockAppContext({ account, session: mockSession(false) })}
67+
value={mockAppContext({
68+
account,
69+
session: mockSession(false),
70+
authClient: {
71+
sessionStatus: () => {
72+
return {
73+
details: {
74+
sessionVerified: false,
75+
},
76+
};
77+
},
78+
} as unknown as AuthClient,
79+
})}
6880
>
6981
<VerifiedSessionGuard {...{ onDismiss, onError }}>
7082
<div>Content</div>

packages/fxa-settings/src/models/mocks.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ export function mockAppContext(context?: AppContextValue) {
204204
{
205205
account: MOCK_ACCOUNT,
206206
session: mockSession(),
207-
authClient: mockAuthClient(),
208207
config: getDefault(),
209208
sensitiveDataClient: mockSensitiveDataClient(),
210209
uniqueUserId: '4a9512ac-3110-43df-aa8a-958A3d210b9c3',

packages/fxa-settings/src/pages/Signin/index.stories.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,24 @@ export const SignInRelayWithPasswordlessSupport = storyWithProps({
139139
supportsKeysOptionalLogin: true,
140140
});
141141

142+
export const CachedSignInAiWindowWithPasswordlessSupport = storyWithProps({
143+
sessionToken: MOCK_SESSION_TOKEN,
144+
integration: createMockSigninOAuthNativeIntegration({
145+
service: OAuthNativeServices.AiWindow,
146+
isSync: false,
147+
}),
148+
supportsKeysOptionalLogin: true,
149+
});
150+
151+
export const CachedSignInRelayWithPasswordlessSupport = storyWithProps({
152+
sessionToken: MOCK_SESSION_TOKEN,
153+
integration: createMockSigninOAuthNativeIntegration({
154+
service: OAuthNativeServices.Relay,
155+
isSync: false,
156+
}),
157+
supportsKeysOptionalLogin: true,
158+
});
159+
142160
export const SignInAiWindowWithPasswordlessSupport = storyWithProps({
143161
integration: createMockSigninOAuthNativeIntegration({
144162
service: OAuthNativeServices.AiWindow,

packages/fxa-settings/src/pages/Signin/index.test.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,81 @@ describe('Signin component', () => {
10311031
passwordInputNotRendered();
10321032
});
10331033

1034+
it('shows cached signin for service=relay when supportsKeysOptionalLogin is true', () => {
1035+
const integration = createMockSigninOAuthNativeIntegration({
1036+
service: OAuthNativeServices.Relay,
1037+
isSync: false,
1038+
});
1039+
render({
1040+
integration,
1041+
sessionToken: MOCK_SESSION_TOKEN,
1042+
supportsKeysOptionalLogin: true,
1043+
});
1044+
1045+
passwordInputNotRendered();
1046+
expect(GleanMetrics.cachedLogin.view).toHaveBeenCalledWith({
1047+
event: { thirdPartyLinks: true },
1048+
});
1049+
});
1050+
1051+
it('shows cached signin for service=aiwindow when supportsKeysOptionalLogin is true', () => {
1052+
const integration = createMockSigninOAuthNativeIntegration({
1053+
service: OAuthNativeServices.AiWindow,
1054+
isSync: false,
1055+
});
1056+
render({
1057+
integration,
1058+
sessionToken: MOCK_SESSION_TOKEN,
1059+
supportsKeysOptionalLogin: true,
1060+
});
1061+
1062+
passwordInputNotRendered();
1063+
expect(GleanMetrics.cachedLogin.view).toHaveBeenCalledWith({
1064+
event: { thirdPartyLinks: true },
1065+
});
1066+
});
1067+
1068+
it('requires password for service=relay when supportsKeysOptionalLogin is false', () => {
1069+
const integration = createMockSigninOAuthNativeIntegration({
1070+
service: OAuthNativeServices.Relay,
1071+
isSync: false,
1072+
});
1073+
render({
1074+
integration,
1075+
sessionToken: MOCK_SESSION_TOKEN,
1076+
supportsKeysOptionalLogin: false,
1077+
});
1078+
1079+
passwordInputRendered();
1080+
expect(GleanMetrics.login.view).toHaveBeenCalledWith({
1081+
event: { thirdPartyLinks: false },
1082+
});
1083+
});
1084+
1085+
it('sends webchannel message if cached signin for service=relay when supportsKeysOptionalLogin is true', async () => {
1086+
const fxaLoginSpy = jest.spyOn(firefox, 'fxaLogin');
1087+
1088+
const integration = createMockSigninOAuthNativeIntegration({
1089+
service: OAuthNativeServices.Relay,
1090+
isSync: false,
1091+
});
1092+
render({
1093+
integration,
1094+
sessionToken: MOCK_SESSION_TOKEN,
1095+
supportsKeysOptionalLogin: true,
1096+
});
1097+
await submit();
1098+
await waitFor(() => {
1099+
expect(fxaLoginSpy).toHaveBeenCalledWith({
1100+
email: MOCK_EMAIL,
1101+
sessionToken: MOCK_SESSION_TOKEN,
1102+
uid: MOCK_UID,
1103+
verified: true,
1104+
services: { relay: {} },
1105+
});
1106+
});
1107+
});
1108+
10341109
it('emits an event on forgot password link click', async () => {
10351110
renderWithLocalizationProvider(
10361111
<Subject sessionToken={MOCK_SESSION_TOKEN} />

packages/fxa-settings/src/pages/Signin/index.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useNavigateWithQuery } from '../../lib/hooks/useNavigateWithQuery';
77
import classNames from 'classnames';
88
import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
99
import { FtlMsg } from 'fxa-react/lib/utils';
10-
import React, { useCallback, useEffect, useRef, useState } from 'react';
10+
import React, { useCallback, useEffect, useState } from 'react';
1111
import { useForm } from 'react-hook-form';
1212
import AppLayout from '../../components/AppLayout';
1313
import CardHeader from '../../components/CardHeader';
@@ -92,12 +92,14 @@ const Signin = ({
9292
const isServiceWithEmailVerification =
9393
!!clientId && config.servicesWithEmailVerification.includes(clientId);
9494

95-
// We must use a ref because we may update this value in a callback
96-
let isPasswordNeededRef = useRef(
97-
(!sessionToken && hasPassword) ||
95+
const [hasCachedAccount, setHasCachedAccount] =
96+
useState<boolean>(!!sessionToken);
97+
98+
const isPasswordNeeded =
99+
((!hasCachedAccount && hasPassword) ||
98100
integration.wantsKeys() ||
99-
(isOAuth && integration.wantsLogin())
100-
);
101+
(isOAuth && integration.wantsLogin())) &&
102+
!(hasCachedAccount && supportsKeysOptionalLogin);
101103

102104
const localizedPasswordFormLabel = ftlMsgResolver.getMsg(
103105
'signin-password-button-label',
@@ -127,7 +129,7 @@ const Signin = ({
127129
: isOAuthNative && !supportsKeysOptionalLogin;
128130

129131
useEffect(() => {
130-
if (!isPasswordNeededRef.current) {
132+
if (!isPasswordNeeded) {
131133
GleanMetrics.cachedLogin.view({
132134
event: { thirdPartyLinks: !hideThirdPartyAuth },
133135
});
@@ -136,7 +138,7 @@ const Signin = ({
136138
event: { thirdPartyLinks: !hideThirdPartyAuth },
137139
});
138140
}
139-
}, [isPasswordNeededRef, hideThirdPartyAuth]);
141+
}, [isPasswordNeeded, hideThirdPartyAuth]);
140142

141143
const signInWithCachedAccount = useCallback(
142144
async (sessionToken: hexstring) => {
@@ -167,6 +169,8 @@ const Signin = ({
167169
queryParams: location.search,
168170
performNavigation: !integration.isFirefoxMobileClient(),
169171
isServiceWithEmailVerification,
172+
handleFxaLogin: true,
173+
handleFxaOAuthLogin: true,
170174
};
171175

172176
const { error: navError } = await handleNavigation(navigationOptions);
@@ -181,7 +185,7 @@ const Signin = ({
181185
getLocalizedErrorMessage(ftlMsgResolver, error)
182186
);
183187
if (error.errno === AuthUiErrors.SESSION_EXPIRED.errno) {
184-
isPasswordNeededRef.current = true;
188+
setHasCachedAccount(false);
185189
}
186190
setSigninLoading(false);
187191
}
@@ -339,19 +343,19 @@ const Signin = ({
339343

340344
const onSubmit = useCallback(
341345
async ({ password }: { password: string }) => {
342-
if (isPasswordNeededRef.current && password === '') {
346+
if (isPasswordNeeded && password === '') {
343347
setPasswordTooltipErrorText(localizedValidPasswordError);
344348
return;
345349
}
346350

347-
!isPasswordNeededRef.current && sessionToken
351+
!isPasswordNeeded && sessionToken
348352
? signInWithCachedAccount(sessionToken)
349353
: signInWithPassword(password);
350354
},
351355
[
352356
signInWithCachedAccount,
353357
signInWithPassword,
354-
isPasswordNeededRef,
358+
isPasswordNeeded,
355359
localizedValidPasswordError,
356360
sessionToken,
357361
]
@@ -372,7 +376,7 @@ const Signin = ({
372376
}}
373377
/>
374378
)}
375-
{isPasswordNeededRef.current && hasPassword ? (
379+
{isPasswordNeeded && hasPassword ? (
376380
<CardHeader
377381
headingText="Enter your password"
378382
headingAndSubheadingFtlId="signin-password-needed-header-2"
@@ -441,11 +445,12 @@ const Signin = ({
441445
</FtlMsg>
442446
)}
443447
</div>
444-
{!hasLinkedAccountAndNoPassword && (
448+
{(!hasLinkedAccountAndNoPassword ||
449+
(hasCachedAccount && supportsKeysOptionalLogin)) && (
445450
<form onSubmit={handleSubmit(onSubmit)}>
446451
<input type="email" className="hidden" value={email} disabled />
447452

448-
{isPasswordNeededRef.current && (
453+
{isPasswordNeeded && (
449454
<InputPassword
450455
name="password"
451456
anchorPosition="start"
@@ -538,7 +543,7 @@ const Signin = ({
538543
}`}
539544
className="text-sm link-blue mx-auto tablet:mx-0"
540545
onClick={() =>
541-
!isPasswordNeededRef.current
546+
!isPasswordNeeded
542547
? GleanMetrics.cachedLogin.forgotPassword()
543548
: GleanMetrics.login.forgotPassword()
544549
}

0 commit comments

Comments
 (0)