зеркало из https://github.com/mozilla/fxa.git
feat(react): Add React signin Sync web channel events for fx_desktop_v3 + oauth_webchannel_v1
Because: * We are moving from Backbone to React and want to meet parity with Sync functionality This commit: * Tweaks config-fxios script since iOS changed directory nesting * Adds firefox.fxaLogin and firefox.fxaOAuthLogin web channel events where needed; it should talk to the browser with a happy path login, signin_token_code flow, and signin_totp_code flow * Return unwrapBKey as part of signin callback data for sync * Always displays password input for Sync (no cached login) * Adds temp 'hack' (tempHandleSyncLogin) to allow a hard navigate to CAD to work in these flows * Fixes bug where we were sending fxaLogin instead of fxaCanLinkAccount. Removed these from signin and signup container pages because we send one on the index page and it's causing multiple Sync dialogs * Renames signinLocationState in signintotpcode to signinState since it can be set to local storage values * Tweaks when to display third party auth for Sync (only show in the Sync flow when user does not have a PW set) closes FXA-9059
This commit is contained in:
Родитель
407c7e3cdb
Коммит
6d8745a2f4
|
@ -19,7 +19,12 @@ const replaceFrom = /\bserver: server\b/;
|
|||
const replaceTo = 'contentUrl: "http://localhost:3030"';
|
||||
|
||||
function replace() {
|
||||
const filePath = path.join(iosPath, 'RustFxA', 'RustFirefoxAccounts.swift');
|
||||
const filePath = path.join(
|
||||
iosPath,
|
||||
'firefox-ios',
|
||||
'RustFxA',
|
||||
'RustFirefoxAccounts.swift'
|
||||
);
|
||||
let fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const match = fileContent.match(regex);
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ export type FxAOAuthLogin = {
|
|||
code: string;
|
||||
redirect: string;
|
||||
state: string;
|
||||
// For sync oauth
|
||||
// For sync oauth signup
|
||||
declinedSyncEngines?: string[];
|
||||
offeredSyncEngines?: string[];
|
||||
};
|
||||
|
@ -337,7 +337,7 @@ export class Firefox extends EventTarget {
|
|||
}
|
||||
|
||||
fxaCanLinkAccount(options: FxACanLinkAccount) {
|
||||
this.send(FirefoxCommand.Login, options);
|
||||
this.send(FirefoxCommand.CanLinkAccount, options);
|
||||
}
|
||||
|
||||
async requestSignedInUser(context: string) {
|
||||
|
|
|
@ -131,9 +131,7 @@ describe('SigninTokenCode container', () => {
|
|||
await waitFor(() => {
|
||||
expect(CacheModule.currentAccount).not.toBeCalled();
|
||||
});
|
||||
expect(currentSigninTokenCodeProps?.signinLocationState.email).toBe(
|
||||
MOCK_EMAIL
|
||||
);
|
||||
expect(currentSigninTokenCodeProps?.signinState.email).toBe(MOCK_EMAIL);
|
||||
expect(currentSigninTokenCodeProps?.integration).toBe(integration);
|
||||
expect(SigninTokenCodeModule.default).toBeCalled();
|
||||
});
|
||||
|
@ -142,7 +140,7 @@ describe('SigninTokenCode container', () => {
|
|||
render();
|
||||
expect(CacheModule.currentAccount).not.toBeCalled();
|
||||
await waitFor(() => {
|
||||
expect(currentSigninTokenCodeProps?.signinLocationState.email).toBe(
|
||||
expect(currentSigninTokenCodeProps?.signinState.email).toBe(
|
||||
MOCK_EMAIL
|
||||
);
|
||||
});
|
||||
|
@ -154,7 +152,7 @@ describe('SigninTokenCode container', () => {
|
|||
render();
|
||||
expect(CacheModule.currentAccount).toBeCalled();
|
||||
await waitFor(() => {
|
||||
expect(currentSigninTokenCodeProps?.signinLocationState.email).toBe(
|
||||
expect(currentSigninTokenCodeProps?.signinState.email).toBe(
|
||||
MOCK_STORED_ACCOUNT.email
|
||||
);
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ const SigninTokenCodeContainer = ({
|
|||
state?: SigninLocationState;
|
||||
};
|
||||
|
||||
const signinLocationState =
|
||||
const signinState =
|
||||
location.state && Object.keys(location.state).length > 0
|
||||
? location.state
|
||||
: getStoredAccountInfo();
|
||||
|
@ -44,8 +44,8 @@ const SigninTokenCodeContainer = ({
|
|||
const { data: totpData, loading: totpLoading } =
|
||||
useQuery<TotpStatusResponse>(GET_TOTP_STATUS);
|
||||
|
||||
if (Object.keys(signinLocationState).length < 1) {
|
||||
hardNavigateToContentServer(`/${location.search ? location.search : ''}`);
|
||||
if (Object.keys(signinState).length < 1) {
|
||||
hardNavigateToContentServer(`/${location.search || ''}`);
|
||||
return <LoadingSpinner fullScreen />;
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ const SigninTokenCodeContainer = ({
|
|||
{...{
|
||||
finishOAuthFlowHandler,
|
||||
integration,
|
||||
signinLocationState,
|
||||
signinState,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,8 +18,13 @@ import { Subject } from './mocks';
|
|||
import { MOCK_SIGNUP_CODE } from '../../Signup/ConfirmSignupCode/mocks';
|
||||
import { MOCK_EMAIL, MOCK_OAUTH_FLOW_HANDLER_RESPONSE } from '../../mocks';
|
||||
import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors';
|
||||
import { createMockSigninOAuthIntegration } from '../mocks';
|
||||
import {
|
||||
createMockSigninOAuthIntegration,
|
||||
createMockSigninSyncIntegration,
|
||||
} from '../mocks';
|
||||
import { createMockSigninLocationState } from './mocks';
|
||||
import VerificationReasons from '../../../constants/verification-reasons';
|
||||
import firefox from '../../../lib/channels/firefox';
|
||||
|
||||
jest.mock('../../../lib/metrics', () => ({
|
||||
usePageViewEvent: jest.fn(),
|
||||
|
@ -45,22 +50,13 @@ function applyDefaultMocks() {
|
|||
mockReactUtilsModule();
|
||||
}
|
||||
|
||||
// Set this when testing location state
|
||||
let mockLocationState = {};
|
||||
const mockLocation = () => {
|
||||
return {
|
||||
pathname: '/signin_token_code',
|
||||
search: '?' + new URLSearchParams(mockLocationState),
|
||||
state: mockLocationState,
|
||||
};
|
||||
};
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('@reach/router', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
...jest.requireActual('@reach/router'),
|
||||
useNavigate: () => mockNavigate,
|
||||
useLocation: () => mockLocation(),
|
||||
useLocation: () => () => {},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -129,6 +125,24 @@ describe('SigninTokenCode page', () => {
|
|||
expect(GleanMetrics.loginConfirmation.view).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('fxaLogin webchannel message', () => {
|
||||
let fxaLoginSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
fxaLoginSpy = jest.spyOn(firefox, 'fxaLogin');
|
||||
});
|
||||
it('is sent if Sync integration', () => {
|
||||
const integration = createMockSigninSyncIntegration();
|
||||
render({ integration });
|
||||
expect(fxaLoginSpy).toHaveBeenCalledWith({
|
||||
...createMockSigninLocationState(integration.wantsKeys()),
|
||||
});
|
||||
});
|
||||
it('is not sent otherwise', () => {
|
||||
render();
|
||||
expect(fxaLoginSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleResendCode submission', () => {
|
||||
async function renderAndResend() {
|
||||
render();
|
||||
|
|
|
@ -26,6 +26,7 @@ import Banner, {
|
|||
ResendEmailSuccessBanner,
|
||||
} from '../../../components/Banner';
|
||||
import { handleNavigation } from '../utils';
|
||||
import firefox from '../../../lib/channels/firefox';
|
||||
|
||||
export const viewName = 'signin-token-code';
|
||||
|
||||
|
@ -34,7 +35,7 @@ const SIX_DIGIT_NUMBER_REGEX = /^\d{6}$/;
|
|||
const SigninTokenCode = ({
|
||||
finishOAuthFlowHandler,
|
||||
integration,
|
||||
signinLocationState,
|
||||
signinState,
|
||||
}: SigninTokenCodeProps & RouteComponentProps) => {
|
||||
usePageViewEvent(viewName, REACT_ENTRYPOINT);
|
||||
const session = useSession();
|
||||
|
@ -48,7 +49,7 @@ const SigninTokenCode = ({
|
|||
verificationReason,
|
||||
keyFetchToken,
|
||||
unwrapBKey,
|
||||
} = signinLocationState;
|
||||
} = signinState;
|
||||
|
||||
const [banner, setBanner] = useState<Partial<BannerProps>>({
|
||||
type: undefined,
|
||||
|
@ -81,6 +82,24 @@ const SigninTokenCode = ({
|
|||
GleanMetrics.loginConfirmation.view();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (integration.isSync()) {
|
||||
await firefox.fxaLogin({
|
||||
email,
|
||||
// keyFetchToken and unwrapBKey should always exist if Sync integration
|
||||
keyFetchToken: keyFetchToken!,
|
||||
unwrapBKey: unwrapBKey!,
|
||||
sessionToken,
|
||||
uid,
|
||||
verified: false,
|
||||
});
|
||||
}
|
||||
})();
|
||||
// Only send webchannel message if sync on initial render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleAnimationEnd = () => {
|
||||
// We add the "shake" animation to bring attention to the success banner
|
||||
// when the success banner was already displayed. We have to remove the
|
||||
|
|
|
@ -9,7 +9,7 @@ import { SigninIntegration, SigninLocationState } from '../interfaces';
|
|||
export interface SigninTokenCodeProps {
|
||||
finishOAuthFlowHandler: FinishOAuthFlowHandler;
|
||||
integration: SigninIntegration;
|
||||
signinLocationState: SigninLocationState;
|
||||
signinState: SigninLocationState;
|
||||
}
|
||||
|
||||
export interface TotpStatusResponse {
|
||||
|
|
|
@ -3,13 +3,15 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { LocationProvider } from '@reach/router';
|
||||
import { Integration, IntegrationType } from '../../../models';
|
||||
import { IntegrationType } from '../../../models';
|
||||
import { SigninTokenCodeProps } from './interfaces';
|
||||
import SigninTokenCode from '.';
|
||||
import {
|
||||
MOCK_EMAIL,
|
||||
MOCK_KEY_FETCH_TOKEN,
|
||||
MOCK_SESSION_TOKEN,
|
||||
MOCK_UID,
|
||||
MOCK_UNWRAP_BKEY,
|
||||
mockFinishOAuthFlowHandler,
|
||||
} from '../../mocks';
|
||||
import { MozServices } from '../../../lib/types';
|
||||
|
@ -21,10 +23,11 @@ export function createMockWebIntegration() {
|
|||
getService: () => MozServices.Default,
|
||||
isSync: () => false,
|
||||
wantsKeys: () => false,
|
||||
} as Integration;
|
||||
};
|
||||
}
|
||||
|
||||
export const createMockSigninLocationState = (
|
||||
wantsKeys = false,
|
||||
verificationReason?: VerificationReasons
|
||||
) => {
|
||||
return {
|
||||
|
@ -32,7 +35,11 @@ export const createMockSigninLocationState = (
|
|||
uid: MOCK_UID,
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
verified: false,
|
||||
...(verificationReason && { verificationReason }),
|
||||
verificationReason,
|
||||
...(wantsKeys && {
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -50,7 +57,10 @@ export const Subject = ({
|
|||
finishOAuthFlowHandler,
|
||||
integration,
|
||||
}}
|
||||
signinLocationState={createMockSigninLocationState(verificationReason)}
|
||||
signinState={createMockSigninLocationState(
|
||||
integration.wantsKeys(),
|
||||
verificationReason
|
||||
)}
|
||||
/>
|
||||
</LocationProvider>
|
||||
);
|
||||
|
|
|
@ -16,9 +16,14 @@ import {
|
|||
} from '../../../lib/auth-errors/auth-errors';
|
||||
import { Subject } from './mocks';
|
||||
import { MOCK_OAUTH_FLOW_HANDLER_RESPONSE } from '../../mocks';
|
||||
import { createMockSigninOAuthIntegration } from '../mocks';
|
||||
import {
|
||||
createMockSigninOAuthIntegration,
|
||||
createMockSigninSyncIntegration,
|
||||
} from '../mocks';
|
||||
import { SigninIntegration } from '../interfaces';
|
||||
import { FinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
|
||||
import firefox from '../../../lib/channels/firefox';
|
||||
import * as utils from 'fxa-react/lib/utils';
|
||||
|
||||
jest.mock('../../../lib/metrics', () => ({
|
||||
usePageViewEvent: jest.fn(),
|
||||
|
@ -142,6 +147,41 @@ describe('Sign in with TOTP code page', () => {
|
|||
expect(mockNavigate).toHaveBeenCalledWith('/settings');
|
||||
});
|
||||
|
||||
// When CAD is converted to React, just test navigation since CAD will handle fxaLogin
|
||||
describe('fxaLogin webchannel message (tempHandleSyncLogin)', () => {
|
||||
let fxaLoginSpy: jest.SpyInstance;
|
||||
let hardNavigateSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
fxaLoginSpy = jest.spyOn(firefox, 'fxaLogin');
|
||||
hardNavigateSpy = jest
|
||||
.spyOn(utils, 'hardNavigate')
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
it('is sent if Sync integration and navigates to CAD', async () => {
|
||||
const integration = createMockSigninSyncIntegration();
|
||||
await waitFor(() =>
|
||||
renderAndSubmitTotpCode(
|
||||
{
|
||||
status: true,
|
||||
},
|
||||
undefined,
|
||||
integration
|
||||
)
|
||||
);
|
||||
expect(fxaLoginSpy).toHaveBeenCalled();
|
||||
expect(hardNavigateSpy).toHaveBeenCalledWith(
|
||||
'/connect_another_device?showSuccessMessage=true'
|
||||
);
|
||||
});
|
||||
it('is not sent otherwise', async () => {
|
||||
await renderAndSubmitTotpCode({
|
||||
status: true,
|
||||
});
|
||||
expect(fxaLoginSpy).not.toHaveBeenCalled();
|
||||
expect(hardNavigateSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error on invalid code', async () => {
|
||||
await renderAndSubmitTotpCode({
|
||||
status: false,
|
||||
|
@ -167,7 +207,7 @@ describe('Sign in with TOTP code page', () => {
|
|||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('withOAuth integration', () => {
|
||||
describe('with OAuth integration', () => {
|
||||
it('navigates to relying party on success', async () => {
|
||||
const finishOAuthFlowHandler = jest
|
||||
.fn()
|
||||
|
|
|
@ -145,7 +145,7 @@ export const SigninTotpCode = ({
|
|||
queryParams: location.search,
|
||||
};
|
||||
|
||||
handleNavigation(navigationOptions, navigate);
|
||||
await handleNavigation(navigationOptions, navigate, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -385,25 +385,6 @@ describe('signin container', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('web channel messages', () => {
|
||||
let fxaLoginSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
fxaLoginSpy = jest.spyOn(firefox, 'fxaCanLinkAccount');
|
||||
});
|
||||
|
||||
// TODO in Sync ticket. Is this being sent under the right conditions?
|
||||
// it('are sent for sync', async () => {
|
||||
// mockSyncDesktopV3Integration();
|
||||
// render();
|
||||
// await waitFor(() => {
|
||||
// expect(fxaLoginSpy).toBeCalledWith({ email: MOCK_QUERY_PARAM_EMAIL });
|
||||
// });
|
||||
// });
|
||||
it('are not sent for non-sync', () => {
|
||||
render([mockGqlAvatarUseQuery()]);
|
||||
expect(fxaLoginSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasLinkedAccount and hasPassword are provided', () => {
|
||||
|
|
|
@ -6,8 +6,6 @@ import { RouteComponentProps, useLocation, useNavigate } from '@reach/router';
|
|||
import Signin from '.';
|
||||
import {
|
||||
Integration,
|
||||
isOAuthIntegration,
|
||||
isSyncDesktopV3Integration,
|
||||
useAuthClient,
|
||||
useFtlMsgResolver,
|
||||
useConfig,
|
||||
|
@ -16,7 +14,6 @@ import { MozServices } from '../../lib/types';
|
|||
import { useValidatedQueryParams } from '../../lib/hooks/useValidate';
|
||||
import { SigninQueryParams } from '../../models/pages/signin';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import firefox from '../../lib/channels/firefox';
|
||||
import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
|
||||
import { cache, currentAccount, discardSessionToken } from '../../lib/cache';
|
||||
import { FetchResult, useMutation, useQuery } from '@apollo/client';
|
||||
|
@ -138,10 +135,7 @@ const SigninContainer = ({
|
|||
queryParamModel.email || emailFromLocationState
|
||||
);
|
||||
|
||||
const isOAuth = isOAuthIntegration(integration);
|
||||
const isSyncOAuth = isOAuth && integration.isSync();
|
||||
const isSyncDesktopV3 = isSyncDesktopV3Integration(integration);
|
||||
const isSyncWebChannel = isSyncOAuth || isSyncDesktopV3;
|
||||
const wantsKeys = integration.wantsKeys();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -162,31 +156,24 @@ const SigninContainer = ({
|
|||
// For now, just pass back emailStatusChecked. When we convert the Index page
|
||||
// we'll want to read from router state.
|
||||
navigate(`/signup?email=${email}&emailStatusChecked=true`);
|
||||
// TODO: Probably move this to the Index page onsubmit once
|
||||
// the index page is converted to React, we need to run it in
|
||||
// signup and signin for Sync
|
||||
} else {
|
||||
// TODO: in FXA-9177, also set hasLinkedAccount and hasPassword in Apollo cache
|
||||
setAccountStatus({
|
||||
hasLinkedAccount,
|
||||
hasPassword,
|
||||
});
|
||||
if (isSyncWebChannel) {
|
||||
firefox.fxaCanLinkAccount({ email });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
hardNavigateToContentServer(`/?prefillEmail=${email}`);
|
||||
}
|
||||
} else if (isSyncWebChannel) {
|
||||
// TODO: Probably move this to the Index page onsubmit once
|
||||
// the index page is converted to React, we need to run it in
|
||||
// signup and signin for Sync
|
||||
firefox.fxaCanLinkAccount({ email });
|
||||
}
|
||||
} else {
|
||||
hardNavigateToContentServer('/');
|
||||
}
|
||||
})();
|
||||
});
|
||||
// Only run this on initial render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { data: avatarData, loading: avatarLoading } =
|
||||
useQuery<AvatarResponse>(AVATAR_QUERY);
|
||||
|
@ -214,7 +201,7 @@ const SigninContainer = ({
|
|||
const service = integration.getService();
|
||||
const options = {
|
||||
verificationMethod: VerificationMethods.EMAIL_OTP,
|
||||
keys: integration.wantsKeys(),
|
||||
keys: wantsKeys,
|
||||
...(service !== MozServices.Default && { service }),
|
||||
};
|
||||
|
||||
|
@ -296,10 +283,10 @@ const SigninContainer = ({
|
|||
password,
|
||||
clientSalt:
|
||||
credentialStatusData.data?.credentialStatus.clientSalt ||
|
||||
(await createSaltV2()),
|
||||
createSaltV2(),
|
||||
});
|
||||
|
||||
const kB = await unwrapKB(wrapKb, v1Credentials.unwrapBKey);
|
||||
const kB = unwrapKB(wrapKb, v1Credentials.unwrapBKey);
|
||||
const keys = await getKeysV2({
|
||||
kB,
|
||||
v1: v1Credentials,
|
||||
|
@ -347,7 +334,17 @@ const SigninContainer = ({
|
|||
},
|
||||
});
|
||||
|
||||
return { data };
|
||||
if (data) {
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
...(wantsKeys && {
|
||||
unwrapBKey: v2Credentials.unwrapBKey,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return { data: undefined };
|
||||
} catch (error) {
|
||||
return handleGQLError(error);
|
||||
}
|
||||
|
@ -370,7 +367,18 @@ const SigninContainer = ({
|
|||
},
|
||||
},
|
||||
});
|
||||
return { data };
|
||||
|
||||
if (data) {
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
...(wantsKeys && {
|
||||
unwrapBKey: v1Credentials.unwrapBKey,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return { data: undefined };
|
||||
} catch (error) {
|
||||
// TODO consider additional error handling - any non-gql errors will return an unexpected error
|
||||
return handleGQLError(error);
|
||||
|
@ -385,6 +393,7 @@ const SigninContainer = ({
|
|||
keyStretchExp.queryParamModel,
|
||||
passwordChangeFinish,
|
||||
passwordChangeStart,
|
||||
wantsKeys,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -6,7 +6,11 @@ import React from 'react';
|
|||
import Signin from '.';
|
||||
import { MozServices } from '../../lib/types';
|
||||
import { Meta } from '@storybook/react';
|
||||
import { Subject, createMockSigninSyncIntegration } from './mocks';
|
||||
import {
|
||||
Subject,
|
||||
createMockSigninOAuthIntegration,
|
||||
createMockSigninSyncIntegration,
|
||||
} from './mocks';
|
||||
import { withLocalization } from 'fxa-react/lib/storybooks';
|
||||
import { SigninProps } from './interfaces';
|
||||
import { MOCK_SERVICE, MOCK_SESSION_TOKEN } from '../mocks';
|
||||
|
@ -28,9 +32,10 @@ export const SignInToSettingsWithPassword = storyWithProps();
|
|||
export const SignInToRelyingPartyWithPassword = storyWithProps({
|
||||
serviceName: MOCK_SERVICE,
|
||||
});
|
||||
// TODO with OAuth ticket, needs OAuth integration
|
||||
|
||||
export const SignInToPocketWithPassword = storyWithProps({
|
||||
serviceName: MozServices.Pocket,
|
||||
integration: createMockSigninOAuthIntegration(),
|
||||
});
|
||||
|
||||
export const SignInToSettingsWithCachedCredentials = storyWithProps({
|
||||
|
@ -40,10 +45,16 @@ export const SignInToRelyingPartyWithCachedCredentials = storyWithProps({
|
|||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
serviceName: MOCK_SERVICE,
|
||||
});
|
||||
// TODO with OAuth ticket, needs OAuth integration
|
||||
|
||||
export const SignInToPocketWithCachedCredentials = storyWithProps({
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
serviceName: MozServices.Pocket,
|
||||
integration: createMockSigninOAuthIntegration(),
|
||||
});
|
||||
|
||||
export const SignInToSyncWithCachedCredentials = storyWithProps({
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
integration: createMockSigninOAuthIntegration({ wantsKeys: true }),
|
||||
});
|
||||
|
||||
export const HasLinkedAccountAndNoPassword = storyWithProps({
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
createBeginSigninResponseError,
|
||||
createCachedSigninResponseError,
|
||||
createMockSigninOAuthIntegration,
|
||||
createMockSigninSyncIntegration,
|
||||
Subject,
|
||||
} from './mocks';
|
||||
import {
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
MONITOR_CLIENTIDS,
|
||||
POCKET_CLIENTIDS,
|
||||
} from '../../models/integrations/client-matching';
|
||||
import firefox from '../../lib/channels/firefox';
|
||||
// import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';
|
||||
// import { FluentBundle } from '@fluent/bundle';
|
||||
jest.mock('../../lib/metrics', () => ({
|
||||
|
@ -173,6 +175,19 @@ describe('Signin', () => {
|
|||
differentAccountLinkRendered();
|
||||
});
|
||||
|
||||
it('does not render third party auth for sync', () => {
|
||||
const integration = createMockSigninSyncIntegration();
|
||||
render({ integration });
|
||||
enterPasswordAndSubmit();
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /Continue with Google/ })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /Continue with Apple/ })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('emits an event on forgot password link click', async () => {
|
||||
render();
|
||||
fireEvent.click(screen.getByText('Forgot password?'));
|
||||
|
@ -306,82 +321,159 @@ describe('Signin', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('OAuth integration', () => {
|
||||
describe('wants keys', () => {
|
||||
it('navigates to /confirm_signup_code with router state including keys', async () => {
|
||||
const beginSigninHandler = jest.fn().mockReturnValueOnce(
|
||||
createBeginSigninResponse({
|
||||
verified: false,
|
||||
verificationReason: VerificationReasons.SIGN_UP,
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
})
|
||||
);
|
||||
const finishOAuthFlowHandler = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(MOCK_OAUTH_FLOW_HANDLER_RESPONSE);
|
||||
const integration = createMockSigninOAuthIntegration();
|
||||
render({
|
||||
beginSigninHandler,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
});
|
||||
// When CAD is converted to React, just test navigation since CAD will handle fxaLogin
|
||||
describe('fxaLogin webchannel message (tempHandleSyncLogin)', () => {
|
||||
let fxaLoginSpy: jest.SpyInstance;
|
||||
let hardNavigateSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
fxaLoginSpy = jest.spyOn(firefox, 'fxaLogin');
|
||||
hardNavigateSpy = jest
|
||||
.spyOn(utils, 'hardNavigate')
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
it('is sent if Sync integration and navigates to CAD', async () => {
|
||||
const beginSigninHandler = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(createBeginSigninResponse());
|
||||
const integration = createMockSigninSyncIntegration();
|
||||
render({ beginSigninHandler, integration });
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(fxaLoginSpy).toHaveBeenCalled();
|
||||
});
|
||||
expect(hardNavigateSpy).toHaveBeenCalledWith(
|
||||
'/connect_another_device?showSuccessMessage=true'
|
||||
);
|
||||
});
|
||||
it('is not sent otherwise', async () => {
|
||||
render();
|
||||
enterPasswordAndSubmit();
|
||||
expect(fxaLoginSpy).not.toHaveBeenCalled();
|
||||
expect(hardNavigateSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
'/confirm_signup_code',
|
||||
{
|
||||
state: {
|
||||
email: MOCK_EMAIL,
|
||||
uid: MOCK_UID,
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
verified: false,
|
||||
verificationReason: 'signup',
|
||||
verificationMethod: 'email-otp',
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
describe('OAuth integration', () => {
|
||||
let fxaOAuthLoginSpy: jest.SpyInstance;
|
||||
let hardNavigateSpy: jest.SpyInstance;
|
||||
let finishOAuthFlowHandler: jest.Mock;
|
||||
beforeEach(() => {
|
||||
fxaOAuthLoginSpy = jest.spyOn(firefox, 'fxaOAuthLogin');
|
||||
hardNavigateSpy = jest
|
||||
.spyOn(utils, 'hardNavigate')
|
||||
.mockImplementation(() => {});
|
||||
finishOAuthFlowHandler = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(MOCK_OAUTH_FLOW_HANDLER_RESPONSE);
|
||||
});
|
||||
it('unverified, wantsKeys, navigates to /confirm_signup_code with keys', async () => {
|
||||
const beginSigninHandler = jest.fn().mockReturnValueOnce(
|
||||
createBeginSigninResponse({
|
||||
verified: false,
|
||||
verificationReason: VerificationReasons.SIGN_UP,
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
})
|
||||
);
|
||||
const integration = createMockSigninOAuthIntegration();
|
||||
render({
|
||||
beginSigninHandler,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
});
|
||||
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
'/confirm_signup_code',
|
||||
{
|
||||
state: {
|
||||
email: MOCK_EMAIL,
|
||||
uid: MOCK_UID,
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
verified: false,
|
||||
verificationReason: 'signup',
|
||||
verificationMethod: 'email-otp',
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
it('unverified, does not want keys, navigates to /confirm_signup_code without keys', async () => {
|
||||
const beginSigninHandler = jest.fn().mockReturnValueOnce(
|
||||
createBeginSigninResponse({
|
||||
verified: false,
|
||||
verificationReason: VerificationReasons.SIGN_UP,
|
||||
})
|
||||
);
|
||||
const integration = createMockSigninOAuthIntegration();
|
||||
render({
|
||||
beginSigninHandler,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
});
|
||||
|
||||
describe('does not want keys', () => {
|
||||
it('navigates to /confirm_signup_code with router state and no keys', async () => {
|
||||
const beginSigninHandler = jest.fn().mockReturnValueOnce(
|
||||
createBeginSigninResponse({
|
||||
verified: false,
|
||||
verificationReason: VerificationReasons.SIGN_UP,
|
||||
})
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
'/confirm_signup_code',
|
||||
{
|
||||
state: {
|
||||
email: MOCK_EMAIL,
|
||||
uid: MOCK_UID,
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
verified: false,
|
||||
verificationReason: 'signup',
|
||||
verificationMethod: 'email-otp',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
it('verified, not sync, navigates to RP redirect', async () => {
|
||||
const beginSigninHandler = jest.fn().mockReturnValueOnce(
|
||||
createBeginSigninResponse({
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
})
|
||||
);
|
||||
const integration = createMockSigninOAuthIntegration();
|
||||
render({
|
||||
beginSigninHandler,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
});
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(fxaOAuthLoginSpy).not.toHaveBeenCalled();
|
||||
expect(hardNavigateSpy).toHaveBeenCalledWith(
|
||||
MOCK_OAUTH_FLOW_HANDLER_RESPONSE.redirect
|
||||
);
|
||||
});
|
||||
});
|
||||
it('verified, sync, navigates to CAD and sends fxaOAuthLogin', async () => {
|
||||
const beginSigninHandler = jest.fn().mockReturnValueOnce(
|
||||
createBeginSigninResponse({
|
||||
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
|
||||
unwrapBKey: MOCK_UNWRAP_BKEY,
|
||||
})
|
||||
);
|
||||
const integration = createMockSigninOAuthIntegration({
|
||||
isSync: true,
|
||||
});
|
||||
render({
|
||||
beginSigninHandler,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
});
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(fxaOAuthLoginSpy).toHaveBeenCalled();
|
||||
expect(hardNavigateSpy).toHaveBeenCalledWith(
|
||||
'/connect_another_device?showSuccessMessage=true'
|
||||
);
|
||||
const finishOAuthFlowHandler = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(MOCK_OAUTH_FLOW_HANDLER_RESPONSE);
|
||||
const integration = createMockSigninOAuthIntegration();
|
||||
render({
|
||||
beginSigninHandler,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
});
|
||||
|
||||
enterPasswordAndSubmit();
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
'/confirm_signup_code',
|
||||
{
|
||||
state: {
|
||||
email: MOCK_EMAIL,
|
||||
uid: MOCK_UID,
|
||||
sessionToken: MOCK_SESSION_TOKEN,
|
||||
verified: false,
|
||||
verificationReason: 'signup',
|
||||
verificationMethod: 'email-otp',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -583,6 +675,14 @@ describe('with sessionToken', () => {
|
|||
hardNavigateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('always renders password input when integration wants keys', () => {
|
||||
const integration = createMockSigninOAuthIntegration({
|
||||
wantsKeys: true,
|
||||
});
|
||||
render({ integration });
|
||||
passwordInputRendered();
|
||||
});
|
||||
|
||||
it('navigates to OAuth redirect', async () => {
|
||||
const cachedSigninHandler = jest
|
||||
.fn()
|
||||
|
@ -682,10 +782,10 @@ describe('with sessionToken', () => {
|
|||
<Subject
|
||||
sessionToken={MOCK_SESSION_TOKEN}
|
||||
serviceName={MozServices.Pocket}
|
||||
integration={createMockSigninOAuthIntegration(
|
||||
POCKET_CLIENTIDS[0],
|
||||
false
|
||||
)}
|
||||
integration={createMockSigninOAuthIntegration({
|
||||
clientId: POCKET_CLIENTIDS[0],
|
||||
wantsKeys: false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -696,7 +796,9 @@ describe('with sessionToken', () => {
|
|||
it('shows Pocket-specific TOS', () => {
|
||||
renderWithLocalizationProvider(
|
||||
<Subject
|
||||
integration={createMockSigninOAuthIntegration(POCKET_CLIENTIDS[0])}
|
||||
integration={createMockSigninOAuthIntegration({
|
||||
clientId: POCKET_CLIENTIDS[0],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -723,7 +825,9 @@ describe('with sessionToken', () => {
|
|||
it('shows Monitor-specific TOS', async () => {
|
||||
renderWithLocalizationProvider(
|
||||
<Subject
|
||||
integration={createMockSigninOAuthIntegration(MONITOR_CLIENTIDS[0])}
|
||||
integration={createMockSigninOAuthIntegration({
|
||||
clientId: MONITOR_CLIENTIDS[0],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -746,5 +850,3 @@ describe('with sessionToken', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO in FXA-9059: make sure third party auth is not rendered for sync if account has a password
|
||||
|
|
|
@ -79,7 +79,8 @@ const Signin = ({
|
|||
// We must use a ref because we may update this value in a callback
|
||||
let isPasswordNeededRef = useRef(
|
||||
(!sessionToken && hasPassword) ||
|
||||
(isOAuth && (integration.wantsKeys() || integration.wantsLogin()))
|
||||
integration.wantsKeys() ||
|
||||
(isOAuth && integration.wantsLogin())
|
||||
);
|
||||
|
||||
const localizedPasswordFormLabel = ftlMsgResolver.getMsg(
|
||||
|
@ -190,12 +191,13 @@ const Signin = ({
|
|||
email,
|
||||
signinData: data.signIn,
|
||||
unwrapBKey: data.unwrapBKey,
|
||||
verified: data.signIn.verified,
|
||||
integration,
|
||||
finishOAuthFlowHandler,
|
||||
queryParams: location.search,
|
||||
};
|
||||
|
||||
await handleNavigation(navigationOptions, navigate);
|
||||
await handleNavigation(navigationOptions, navigate, true);
|
||||
}
|
||||
if (error) {
|
||||
GleanMetrics.login.error({ reason: error.message });
|
||||
|
@ -289,8 +291,7 @@ const Signin = ({
|
|||
]
|
||||
);
|
||||
|
||||
const hideThirdPartyAuth =
|
||||
integration.isSync() && hasLinkedAccount && hasPassword;
|
||||
const hideThirdPartyAuth = integration.isSync() && hasPassword;
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface AvatarResponse {
|
|||
}
|
||||
|
||||
export type SigninIntegration =
|
||||
| Pick<Integration, 'type' | 'isSync' | 'getService'>
|
||||
| Pick<Integration, 'type' | 'isSync' | 'getService' | 'wantsKeys'>
|
||||
| SigninOAuthIntegration;
|
||||
|
||||
export type SigninOAuthIntegration = Pick<
|
||||
|
@ -160,10 +160,11 @@ export interface NavigationOptions {
|
|||
verified: boolean;
|
||||
verificationMethod?: VerificationMethods;
|
||||
verificationReason?: VerificationReasons;
|
||||
// keyFetchToken and unwrapBKey are included if options.keys=true
|
||||
// These will never exist for the cached signin (prompt=none)
|
||||
// keyFetchToken is included if options.keys=true
|
||||
// This (and unwrapBKey) will never exist for the cached signin (prompt=none)
|
||||
keyFetchToken?: hexstring;
|
||||
};
|
||||
// unwrapBKey is included if integration.wantsKeys()
|
||||
unwrapBKey?: hexstring;
|
||||
integration: SigninIntegration;
|
||||
finishOAuthFlowHandler: FinishOAuthFlowHandler;
|
||||
|
|
|
@ -93,6 +93,7 @@ export function createMockSigninWebIntegration(): SigninIntegration {
|
|||
type: IntegrationType.Web,
|
||||
isSync: () => false,
|
||||
getService: () => MozServices.Default,
|
||||
wantsKeys: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -105,14 +106,19 @@ export function createMockSigninSyncIntegration(): SigninIntegration {
|
|||
};
|
||||
}
|
||||
|
||||
export function createMockSigninOAuthIntegration(
|
||||
clientId?: string,
|
||||
wantsKeys: boolean = true
|
||||
): SigninOAuthIntegration {
|
||||
export function createMockSigninOAuthIntegration({
|
||||
clientId,
|
||||
wantsKeys = true,
|
||||
isSync = false,
|
||||
}: {
|
||||
clientId?: string;
|
||||
wantsKeys?: boolean;
|
||||
isSync?: boolean;
|
||||
} = {}): SigninOAuthIntegration {
|
||||
return {
|
||||
type: IntegrationType.OAuth,
|
||||
getService: () => clientId || MOCK_CLIENT_ID,
|
||||
isSync: () => false,
|
||||
isSync: () => isSync,
|
||||
wantsKeys: () => wantsKeys,
|
||||
wantsLogin: () => false,
|
||||
wantsTwoStepAuthentication: () => false,
|
||||
|
|
|
@ -17,24 +17,56 @@ import {
|
|||
import { isOAuthIntegration } from '../../models';
|
||||
import { NavigateFn } from '@reach/router';
|
||||
import { hardNavigate } from 'fxa-react/lib/utils';
|
||||
import { FinishOAuthFlowHandler } from '../../lib/oauth/hooks';
|
||||
import { currentAccount } from '../../lib/cache';
|
||||
import firefox from '../../lib/channels/firefox';
|
||||
|
||||
// TODO in FXA-9059:
|
||||
// function getSyncNavigate() {
|
||||
// const searchParams = new URLSearchParams(location.search);
|
||||
// searchParams.set('showSuccessMessage', 'true');
|
||||
// const to = `/connect_another_device?${searchParams}`
|
||||
// }
|
||||
interface NavigationTarget {
|
||||
to: string;
|
||||
state?: SigninLocationState;
|
||||
shouldHardNavigate?: boolean;
|
||||
}
|
||||
|
||||
// TODO: don't hard navigate once ConnectAnotherDevice is converted to React
|
||||
export function getSyncNavigate(queryParams: string) {
|
||||
const searchParams = new URLSearchParams(queryParams);
|
||||
searchParams.set('showSuccessMessage', 'true');
|
||||
return {
|
||||
to: `/connect_another_device?${searchParams}`,
|
||||
shouldHardNavigate: true,
|
||||
};
|
||||
}
|
||||
|
||||
// In Backbone and React, 'confirm_signup_code' and 'signin_token_code' send key
|
||||
// and token data up to Sync with fxa_login and then the CAD page (currently
|
||||
// Backbone) completes the signin with fxa_status.
|
||||
//
|
||||
// In Backbone happy path signin (session is verified after signin) as well as
|
||||
// Backbone 'signin_totp_code', we send key/token data up on the CAD page itself
|
||||
// with an fxa_status message. We don't want to do this with React signin until
|
||||
// CAD is converted to React because we'd need to pass this data back to
|
||||
// Backbone. This means temporarily we need to send the sync data up _before_
|
||||
// we hard navigate to CAD in these two flows.
|
||||
export async function handleNavigation(
|
||||
navigationOptions: NavigationOptions,
|
||||
navigate: NavigateFn
|
||||
navigate: NavigateFn,
|
||||
tempHandleSyncLogin = false
|
||||
) {
|
||||
const { to, state, shouldHardNavigate } = await getNavigationTarget(
|
||||
navigationOptions
|
||||
);
|
||||
if (shouldHardNavigate) {
|
||||
if (tempHandleSyncLogin && navigationOptions.integration.isSync()) {
|
||||
firefox.fxaLogin({
|
||||
email: navigationOptions.email,
|
||||
// keyFetchToken and unwrapBKey should always exist if Sync integration
|
||||
keyFetchToken: navigationOptions.signinData.keyFetchToken!,
|
||||
unwrapBKey: navigationOptions.unwrapBKey!,
|
||||
sessionToken: navigationOptions.signinData.sessionToken,
|
||||
uid: navigationOptions.signinData.uid,
|
||||
verified: navigationOptions.signinData.verified,
|
||||
});
|
||||
}
|
||||
|
||||
// Hard navigate to RP, or (temp until CAD is Reactified) CAD
|
||||
hardNavigate(to);
|
||||
return;
|
||||
|
@ -46,46 +78,6 @@ export async function handleNavigation(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getOAuthRedirectAndHandleSync(
|
||||
finishOAuthFlowHandler: FinishOAuthFlowHandler,
|
||||
{
|
||||
uid,
|
||||
sessionToken,
|
||||
keyFetchToken,
|
||||
unwrapBKey,
|
||||
}: {
|
||||
uid: hexstring;
|
||||
sessionToken: hexstring;
|
||||
keyFetchToken?: string;
|
||||
unwrapBKey?: string;
|
||||
}
|
||||
) {
|
||||
const { redirect } =
|
||||
keyFetchToken && unwrapBKey
|
||||
? await finishOAuthFlowHandler(
|
||||
uid,
|
||||
sessionToken,
|
||||
keyFetchToken,
|
||||
unwrapBKey
|
||||
)
|
||||
: await finishOAuthFlowHandler(uid, sessionToken);
|
||||
|
||||
// TODO in FXA-9059 Sync signin ticket. Do we want to do firefox.fxAOAuthLogin here
|
||||
// if the session isn't verified?
|
||||
//
|
||||
// if (integration.isSync()) {
|
||||
// firefox.fxaOAuthLogin({
|
||||
// action: 'signin',
|
||||
// code,
|
||||
// redirect,
|
||||
// state,
|
||||
// })
|
||||
// TODO: don't hard navigate once ConnectAnotherDevice is converted to React
|
||||
// return { to: getSyncNavigate(), shouldHardNavigate: true }
|
||||
// }
|
||||
return { to: redirect, shouldHardNavigate: true };
|
||||
}
|
||||
|
||||
const getNavigationTarget = async ({
|
||||
email,
|
||||
signinData,
|
||||
|
@ -94,7 +86,7 @@ const getNavigationTarget = async ({
|
|||
finishOAuthFlowHandler,
|
||||
redirectTo,
|
||||
queryParams = '',
|
||||
}: NavigationOptions) => {
|
||||
}: NavigationOptions): Promise<NavigationTarget> => {
|
||||
const isOAuth = isOAuthIntegration(integration);
|
||||
const {
|
||||
verified,
|
||||
|
@ -105,73 +97,73 @@ const getNavigationTarget = async ({
|
|||
sessionToken,
|
||||
} = signinData;
|
||||
|
||||
// oAuthResult result will need to be obtained at the next step, once session is verified
|
||||
if (!verified) {
|
||||
const getUnverifiedNav = () => {
|
||||
const state = {
|
||||
email,
|
||||
uid,
|
||||
sessionToken,
|
||||
verified,
|
||||
...(verificationMethod && { verificationMethod }),
|
||||
...(verificationReason && { verificationReason }),
|
||||
...(keyFetchToken && { keyFetchToken }),
|
||||
...(unwrapBKey && { unwrapBKey }),
|
||||
verificationMethod,
|
||||
verificationReason,
|
||||
keyFetchToken,
|
||||
unwrapBKey,
|
||||
};
|
||||
|
||||
// TODO in FXA-9177 Consider storing state in Apollo cache instead of location state
|
||||
if (
|
||||
((verificationReason === VerificationReasons.SIGN_IN ||
|
||||
verificationReason === VerificationReasons.CHANGE_PASSWORD) &&
|
||||
verificationMethod === VerificationMethods.TOTP_2FA) ||
|
||||
(isOAuth && integration.wantsTwoStepAuthentication())
|
||||
) {
|
||||
return {
|
||||
to: `/signin_totp_code${queryParams}`,
|
||||
state,
|
||||
};
|
||||
} else if (verificationReason === VerificationReasons.SIGN_UP) {
|
||||
return {
|
||||
to: `/confirm_signup_code${queryParams}`,
|
||||
state,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
to: `/signin_token_code${queryParams}`,
|
||||
state,
|
||||
};
|
||||
}
|
||||
const getUnverifiedNavTo = () => {
|
||||
// TODO in FXA-9177 Consider storing state in Apollo cache instead of location state
|
||||
if (
|
||||
((verificationReason === VerificationReasons.SIGN_IN ||
|
||||
verificationReason === VerificationReasons.CHANGE_PASSWORD) &&
|
||||
verificationMethod === VerificationMethods.TOTP_2FA) ||
|
||||
(isOAuth && integration.wantsTwoStepAuthentication())
|
||||
) {
|
||||
return `/signin_totp_code${queryParams}`;
|
||||
} else if (verificationReason === VerificationReasons.SIGN_UP) {
|
||||
return `/confirm_signup_code${queryParams}`;
|
||||
}
|
||||
return `/signin_token_code${queryParams}`;
|
||||
};
|
||||
|
||||
return { to: getUnverifiedNavTo(), state };
|
||||
};
|
||||
|
||||
if (!verified) {
|
||||
return getUnverifiedNav();
|
||||
}
|
||||
|
||||
if (verificationReason === VerificationReasons.CHANGE_PASSWORD) {
|
||||
return {
|
||||
to:
|
||||
queryParams.length > 1
|
||||
? `/post_verify/password/force_password_change${queryParams}`
|
||||
: '/post_verify/password/force_password_change',
|
||||
to: `/post_verify/password/force_password_change${
|
||||
(queryParams.length > 1 && queryParams) || ''
|
||||
}`,
|
||||
// TODO in FXA-6653: remove shouldHardNavigate when this route is converted to React
|
||||
shouldHardNavigate: true,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO in FXA-9059 handle sync desktop v3 integration post-sign in navigation
|
||||
|
||||
// oAuthResult can only be obtained when the session is verified
|
||||
// OAuth redirect can only be obtained when the session is verified
|
||||
// otherwise oauth/authorization endpoint throws an "unconfirmed session" error
|
||||
if (verified && isOAuth) {
|
||||
const oAuthResult = await getOAuthRedirectAndHandleSync(
|
||||
finishOAuthFlowHandler,
|
||||
{
|
||||
uid,
|
||||
sessionToken,
|
||||
keyFetchToken,
|
||||
unwrapBKey,
|
||||
}
|
||||
if (isOAuth) {
|
||||
const { redirect, code, state } = await finishOAuthFlowHandler(
|
||||
uid,
|
||||
sessionToken,
|
||||
keyFetchToken,
|
||||
unwrapBKey
|
||||
);
|
||||
|
||||
return {
|
||||
to: oAuthResult.to,
|
||||
shouldHardNavigate: oAuthResult.shouldHardNavigate,
|
||||
};
|
||||
if (integration.isSync()) {
|
||||
firefox.fxaOAuthLogin({
|
||||
action: 'signin',
|
||||
code,
|
||||
redirect,
|
||||
state,
|
||||
});
|
||||
return getSyncNavigate(queryParams);
|
||||
}
|
||||
return { to: redirect, shouldHardNavigate: true };
|
||||
}
|
||||
if (integration.isSync()) {
|
||||
return getSyncNavigate(queryParams);
|
||||
}
|
||||
|
||||
if (redirectTo) {
|
||||
|
|
|
@ -43,6 +43,7 @@ import firefox from '../../../lib/channels/firefox';
|
|||
import GleanMetrics from '../../../lib/glean';
|
||||
import { useWebRedirect } from '../../../lib/hooks/useWebRedirect';
|
||||
import { storeAccountData } from '../../../lib/storage-utils';
|
||||
import { getSyncNavigate } from '../../Signin/utils';
|
||||
|
||||
export const viewName = 'confirm-signup-code';
|
||||
|
||||
|
@ -119,14 +120,6 @@ const ConfirmSignupCode = ({
|
|||
}
|
||||
}
|
||||
|
||||
function navigateToCAD() {
|
||||
// Connect another device tells Sync the user is signed in
|
||||
// TODO: regular navigate when this page is converted
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
searchParams.set('showSuccessMessage', 'true');
|
||||
hardNavigateToContentServer(`/connect_another_device?${searchParams}`);
|
||||
}
|
||||
|
||||
async function verifySession(code: string) {
|
||||
logViewEvent(`flow.${viewName}`, 'submit', REACT_ENTRYPOINT);
|
||||
GleanMetrics.signupConfirmation.submit();
|
||||
|
@ -162,7 +155,8 @@ const ConfirmSignupCode = ({
|
|||
}
|
||||
|
||||
if (isSyncDesktopV3Integration(integration)) {
|
||||
navigateToCAD();
|
||||
const { to } = getSyncNavigate(location.search);
|
||||
hardNavigateToContentServer(to);
|
||||
} else if (isOAuthIntegration(integration)) {
|
||||
// Check to see if the relier wants TOTP.
|
||||
// Newly created accounts wouldn't have this so lets redirect them to signin.
|
||||
|
@ -197,7 +191,8 @@ const ConfirmSignupCode = ({
|
|||
state,
|
||||
});
|
||||
// Mobile sync will close the web view, OAuth Desktop mimics DesktopV3 behavior
|
||||
navigateToCAD();
|
||||
const { to } = getSyncNavigate(location.search);
|
||||
hardNavigateToContentServer(to);
|
||||
return;
|
||||
} else {
|
||||
// Navigate to relying party
|
||||
|
|
|
@ -99,45 +99,38 @@ const SignupContainer = ({
|
|||
const isSyncOAuth = isOAuth && integration.isSync();
|
||||
const isSyncDesktopV3 = isSyncDesktopV3Integration(integration);
|
||||
const isSyncWebChannel = isSyncOAuth || isSyncDesktopV3;
|
||||
const wantsKeys = integration.wantsKeys();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
// Modify this once index is converted to React
|
||||
if (!validationError) {
|
||||
// emailStatusChecked can be passed from React Signin when users hit /signin
|
||||
// with an email query param that we already determined doesn't exist.
|
||||
// It's supplied by Backbone when going from Backbone Index page to React signup.
|
||||
if (!queryParamModel.emailStatusChecked && !emailStatusChecked) {
|
||||
const { exists, hasLinkedAccount, hasPassword } =
|
||||
await authClient.accountStatusByEmail(queryParamModel.email, {
|
||||
thirdPartyAuthStatus: true,
|
||||
// emailStatusChecked can be passed from React Signin when users hit /signin
|
||||
// with an email query param that we already determined doesn't exist.
|
||||
// It's supplied by Backbone when going from Backbone Index page to React signup.
|
||||
if (
|
||||
!validationError &&
|
||||
!queryParamModel.emailStatusChecked &&
|
||||
!emailStatusChecked
|
||||
) {
|
||||
const { exists, hasLinkedAccount, hasPassword } =
|
||||
await authClient.accountStatusByEmail(queryParamModel.email, {
|
||||
thirdPartyAuthStatus: true,
|
||||
});
|
||||
if (exists) {
|
||||
if (config.showReactApp.signInRoutes) {
|
||||
navigate(`/signin`, {
|
||||
replace: true,
|
||||
state: {
|
||||
email: queryParamModel.email,
|
||||
hasLinkedAccount,
|
||||
hasPassword,
|
||||
},
|
||||
});
|
||||
if (exists) {
|
||||
if (config.showReactApp.signInRoutes) {
|
||||
navigate(`/signin`, {
|
||||
replace: true,
|
||||
state: {
|
||||
email: queryParamModel.email,
|
||||
hasLinkedAccount,
|
||||
hasPassword,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
hardNavigateToContentServer(
|
||||
`/signin?email=${queryParamModel.email}`
|
||||
);
|
||||
}
|
||||
// TODO: Probably move this to the Index page onsubmit once
|
||||
// the index page is converted to React, we need to run it in
|
||||
// signup and signin for Sync
|
||||
} else if (isSyncWebChannel) {
|
||||
firefox.fxaCanLinkAccount({ email: queryParamModel.email });
|
||||
} else {
|
||||
hardNavigateToContentServer(
|
||||
`/signin?email=${queryParamModel.email}`
|
||||
);
|
||||
}
|
||||
} else if (isSyncWebChannel) {
|
||||
// TODO: Probably move this to the Index page onsubmit once
|
||||
// the index page is converted to React, we need to run it in
|
||||
// signup and signin for Sync
|
||||
firefox.fxaCanLinkAccount({ email: queryParamModel.email });
|
||||
}
|
||||
}
|
||||
setShowLoadingSpinner(false);
|
||||
|
@ -195,7 +188,7 @@ const SignupContainer = ({
|
|||
const service = integration.getService();
|
||||
const options: BeginSignUpOptions = {
|
||||
verificationMethod: VerificationMethods.EMAIL_OTP,
|
||||
keys: integration.wantsKeys(),
|
||||
keys: wantsKeys,
|
||||
...(service !== MozServices.Default && { service }),
|
||||
atLeast18AtReg,
|
||||
};
|
||||
|
@ -247,9 +240,11 @@ const SignupContainer = ({
|
|||
return {
|
||||
data: {
|
||||
...data,
|
||||
unwrapBKey: credentialsV2
|
||||
? credentialsV2.unwrapBKey
|
||||
: credentialsV1.unwrapBKey,
|
||||
...(wantsKeys && {
|
||||
unwrapBKey: credentialsV2
|
||||
? credentialsV2.unwrapBKey
|
||||
: credentialsV1.unwrapBKey,
|
||||
}),
|
||||
},
|
||||
};
|
||||
} else return { data: undefined };
|
||||
|
@ -258,7 +253,7 @@ const SignupContainer = ({
|
|||
return handleGQLError(error);
|
||||
}
|
||||
},
|
||||
[beginSignup, integration, keyStretchExp, config]
|
||||
[beginSignup, integration, keyStretchExp, config, wantsKeys]
|
||||
);
|
||||
|
||||
// TODO: probably a better way to read this?
|
||||
|
|
|
@ -8,7 +8,6 @@ import { useForm } from 'react-hook-form';
|
|||
import {
|
||||
isOAuthIntegration,
|
||||
isSyncDesktopV3Integration,
|
||||
isSyncOAuthIntegration,
|
||||
useFtlMsgResolver,
|
||||
} from '../../models';
|
||||
import {
|
||||
|
@ -246,10 +245,7 @@ export const Signup = ({
|
|||
const getOfferedSyncEngines = () =>
|
||||
getSyncEngineIds(offeredSyncEngineConfigs || []);
|
||||
|
||||
if (
|
||||
isSyncDesktopV3Integration(integration) ||
|
||||
isSyncOAuthIntegration(integration)
|
||||
) {
|
||||
if (integration.isSync()) {
|
||||
await firefox.fxaLogin({
|
||||
email,
|
||||
// keyFetchToken and unwrapBKey should always exist if Sync integration
|
||||
|
|
Загрузка…
Ссылка в новой задаче