Merge pull request #17816 from mozilla/FXA-10528

refactor(settings): Create OAuthNative integration, rename OAuth integration to OAuthWeb
This commit is contained in:
Lauren Zugai 2024-10-18 15:15:04 -05:00 коммит произвёл GitHub
Родитель 5bf120eb6d 7e7c89b38b
Коммит ad7a75dc8c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
38 изменённых файлов: 608 добавлений и 228 удалений

Просмотреть файл

@ -22,9 +22,9 @@ export type SyncEngineId = EngineConfig['id'] | WebChannelEngineConfig['id'];
/* These sync engines are always offered to the user in Sync fx_desktop_v3
* and other engines can be received and added with a webchannel message.
*
* For OAuth Sync (oauth_webchannel_v1) which includes sync mobile and sync
* desktop on FF 123+, we do not display options by default and instead, we
* receive the webchannel message and overwrite the options.
* For OAuth Sync (oauth_webchannel_v1) which includes sync mobile and
* oauth sync desktop, we do not display options by default and instead,
* we receive the webchannel message and overwrite the options.
*/
export const defaultDesktopV3SyncEngineConfigs = [
{

Просмотреть файл

@ -57,7 +57,7 @@ export class DefaultIntegrationFlags implements IntegrationFlags {
return this.searchParam('context') === Constants.FX_DESKTOP_V3_CONTEXT;
}
// Sync mobile, Sync FF Desktop 123+, and supplicant pairing use this context
// Sync mobile, Sync FF OAuth Desktop, and supplicant pairing use this context
isOAuthWebChannelContext() {
return this.searchParam('context') === Constants.OAUTH_WEBCHANNEL_CONTEXT;
}

Просмотреть файл

@ -8,6 +8,9 @@ import {
Integration,
IntegrationType,
OAuthIntegration,
OAuthNativeClients,
OAuthNativeIntegration,
OAuthWebIntegration,
PairingAuthorityIntegration,
PairingSupplicantIntegration,
RelierClientInfo,
@ -25,6 +28,7 @@ type IntegrationFlagOverrides = {
isOAuth?: boolean;
isServiceSync?: boolean;
isV3DesktopContext?: boolean;
isOAuthWebChannelContext?: boolean;
};
type FactoryCallCounts = {
@ -67,13 +71,14 @@ describe('lib/integrations/integration-factory', () => {
sandbox
.stub(flags, 'isV3DesktopContext')
.returns(!!flagOverrides.isV3DesktopContext);
sandbox
.stub(flags, 'isOAuthWebChannelContext')
.returns(!!flagOverrides.isOAuthWebChannelContext);
urlQueryData.set('scope', 'profile');
urlQueryData.set('client_id', '720bc80adfa6988d');
urlQueryData.set('redirect_uri', 'https://redirect.to');
urlHashData.set('scope', 'profile');
// Create a factory with current state
const factory = new IntegrationFactory({
window,
@ -149,21 +154,9 @@ describe('lib/integrations/integration-factory', () => {
expect(integration.wantsKeys()).toBeFalsy();
expect(integration.isTrusted()).toBeTruthy();
});
// TODO: Remove with approval.
//
// I think maybe this is feature envy, perhaps we should have some dedicated thing that checks integration state
// and account state to determine if features are needed. As far as I can tell the integration models
// themselves really shouldn't know or care about 'accounts'
//
// describe('accountNeedsPermissions', function () {
// it('returns `false`', function () {
// assert.isFalse(integration.accountNeedsPermissions());
// });
// });
});
describe('SyncDesktop creation', () => {
describe('SyncDesktopV3 creation', () => {
const ACTION = 'email';
const CONTEXT = 'fx_desktop_v3';
const COUNTRY = 'RO';
@ -206,7 +199,7 @@ describe('lib/integrations/integration-factory', () => {
// TODO: Port remaining tests from content-server
});
describe('OAuthIntegration creation', () => {
describe('OAuthWebIntegration creation', () => {
let integration: OAuthIntegration;
describe('OAuth redirect', () => {
@ -214,40 +207,66 @@ describe('lib/integrations/integration-factory', () => {
integration = await setup<OAuthIntegration>(
{ isOAuth: true },
{ initIntegration: 1, initOAuthIntegration: 1, initClientInfo: 1 },
(i: Integration) => i instanceof OAuthIntegration
(i: Integration) => i instanceof OAuthWebIntegration
);
});
it('has correct state', async () => {
expect(integration.type).toEqual(IntegrationType.OAuth);
expect(integration.type).toEqual(IntegrationType.OAuthWeb);
expect(integration.isSync()).toBeFalsy();
expect(integration.wantsKeys()).toBeFalsy();
expect(integration.isTrusted()).toBeTruthy();
});
});
describe('OAuth Sync', () => {
// TODO: Port remaining tests from content-server
});
describe('OAuthNativeIntegration creation', () => {
let integration: OAuthNativeIntegration;
describe('without sync', () => {
beforeEach(async () => {
integration = await setup<OAuthIntegration>(
integration = await setup<OAuthNativeIntegration>(
{ isOAuth: true },
{ initIntegration: 1, initOAuthIntegration: 1, initClientInfo: 1 },
(i: Integration) => i instanceof OAuthIntegration
(i: Integration) => i instanceof OAuthNativeIntegration
);
});
it('has correct state', async () => {
expect(integration.type).toEqual(IntegrationType.OAuthWeb);
expect(integration.isSync()).toBeFalsy();
expect(integration.wantsKeys()).toBeFalsy();
expect(integration.isTrusted()).toBeTruthy();
});
});
describe('with sync', () => {
beforeEach(async () => {
integration = await setup<OAuthNativeIntegration>(
{ isOAuth: true, isOAuthWebChannelContext: true },
{ initIntegration: 1, initOAuthIntegration: 1, initClientInfo: 1 },
(i: Integration) => i instanceof OAuthNativeIntegration
);
await mockSearchParams({
scope: Constants.OAUTH_OLDSYNC_SCOPE,
context: Constants.OAUTH_WEBCHANNEL_CONTEXT,
clientId: OAuthNativeClients.FirefoxIOS,
});
sandbox.stub(integration, 'clientInfo').get(() => ({
...clientInfo,
clientId: OAuthNativeClients.FirefoxIOS,
}));
});
it('has correct state', async () => {
expect(integration.type).toEqual(IntegrationType.OAuth);
expect(integration.type).toEqual(IntegrationType.OAuthNative);
expect(integration.isSync()).toBeTruthy();
expect(integration.wantsKeys()).toBeTruthy();
expect(integration.isTrusted()).toBeTruthy();
});
});
// TODO: Port remaining tests from content-server
});
describe('PairingSupplicantIntegration creation', () => {

Просмотреть файл

@ -3,7 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
OAuthIntegration,
OAuthWebIntegration,
OAuthNativeIntegration,
PairingAuthorityIntegration,
PairingSupplicantIntegration,
Integration,
@ -12,6 +13,7 @@ import {
WebIntegration,
RelierClientInfo,
RelierSubscriptionInfo,
OAuthIntegration,
} from '../../models/integrations';
import {
ModelDataStore,
@ -106,7 +108,11 @@ export class IntegrationFactory {
} else if (flags.isDevicePairingAsSupplicant()) {
return this.createPairingSupplicationIntegration(data, storageData);
} else if (flags.isOAuth()) {
return this.createOAuthIntegration(data, storageData);
if (flags.isOAuthWebChannelContext()) {
return this.createOAuthNativeIntegration(data, storageData);
} else {
return this.createOAuthWebIntegration(data, storageData);
}
} else if (flags.isV3DesktopContext()) {
return this.createSyncDesktopV3Integration(data);
} else if (flags.isServiceSync()) {
@ -144,12 +150,31 @@ export class IntegrationFactory {
return integration;
}
private createOAuthIntegration(
private createOAuthWebIntegration(
data: ModelDataStore,
storageData: ModelDataStore
) {
// Resolve configuration settings for oauth relier
const integration = new OAuthIntegration(data, storageData, config.oauth);
const integration = new OAuthWebIntegration(
data,
storageData,
config.oauth
);
this.initIntegration(integration);
this.initOAuthIntegration(integration, this.flags);
this.initClientInfo(integration);
return integration;
}
private createOAuthNativeIntegration(
data: ModelDataStore,
storageData: ModelDataStore
) {
const integration = new OAuthNativeIntegration(
data,
storageData,
config.oauth
);
this.initIntegration(integration);
this.initOAuthIntegration(integration, this.flags);
this.initClientInfo(integration);

Просмотреть файл

@ -9,6 +9,7 @@ export interface IntegrationFlags {
isDevicePairingAsAuthority(): boolean;
isDevicePairingAsSupplicant(): boolean;
isOAuth(): boolean;
isOAuthWebChannelContext(): boolean;
isV3DesktopContext(): boolean;
isOAuthSuccessFlow(): { status: boolean; clientId: string };
isOAuthVerificationFlow(): boolean;

Просмотреть файл

@ -7,8 +7,8 @@ import { useCallback } from 'react';
import {
Integration,
OAuthIntegration,
isOAuthNativeIntegrationSync,
isOAuthIntegration,
isSyncOAuthIntegration,
} from '../../models';
import { createEncryptedBundle } from '../crypto/scoped-keys';
import { Constants } from '../constants';
@ -181,7 +181,7 @@ export function useFinishOAuthFlowHandler(
authClient: AuthClient,
integration: Integration
): UseFinishOAuthFlowHandlerResult {
const isSyncOAuth = isSyncOAuthIntegration(integration);
const isSyncOAuth = isOAuthNativeIntegrationSync(integration);
const finishOAuthFlowHandler: FinishOAuthFlowHandler = useCallback(
async (accountUid, sessionToken, keyFetchToken, unwrapBKey) => {

Просмотреть файл

@ -5,7 +5,8 @@
import { MozServices } from '../../lib/types';
export enum IntegrationType {
OAuth = 'OAuth',
OAuthWeb = 'OAuthWeb', // OAuth for non-browser services/RPs
OAuthNative = 'OAuthNative', // OAuth for desktop & mobile clients
PairingAuthority = 'PairingAuthority', // TODO
PairingSupplicant = 'PairingSupplicant', // TODO
SyncBasic = 'SyncBasic',
@ -96,7 +97,19 @@ export abstract class Integration<
this.features = { ...this.features, ...features } as T;
}
isSync(): boolean {
isSync() {
return false;
}
isDesktopSync() {
return false;
}
isFirefoxMobileClient() {
return false;
}
isFirefoxDesktopClient() {
return false;
}
@ -162,11 +175,6 @@ export abstract class Integration<
isTrusted() {
return true;
}
// TODO: This seems like feature envy... Move logic elsewhere.
// accountNeedsPermissions(account:RelierAccount): boolean {
// return false;
// }
}
export class BaseIntegration<

Просмотреть файл

@ -5,7 +5,8 @@
export * from './base-integration';
export * from './channel-info';
export * from './client-info';
export * from './oauth-integration';
export * from './oauth-web-integration';
export * from './oauth-native-integration';
export * from './pairing-authority-integration';
export * from './pairing-supplicant-integration';
export * from './signin-signup-info';

Просмотреть файл

@ -0,0 +1,146 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 { ModelDataStore, GenericData } from '../../lib/model-data';
import {
OAuthNativeClients,
OAuthNativeIntegration,
} from './oauth-native-integration';
import { OAuthWebIntegration } from './oauth-web-integration';
function mockClientInfo(clientId: string) {
return {
clientId,
serviceName: 'Firefox Sync',
redirectUri: 'https://mock.com',
trusted: true,
imageUri: '',
};
}
describe('OAuthNativeIntegration', function () {
let data: ModelDataStore;
let oauthData: ModelDataStore;
let model: OAuthNativeIntegration;
beforeEach(function () {
data = new GenericData({
clientId: OAuthNativeClients.FirefoxIOS,
service: 'sync',
});
oauthData = new GenericData({
scope: 'profile',
});
model = new OAuthNativeIntegration(data, oauthData, {
scopedKeysEnabled: true,
scopedKeysValidation: {},
isPromptNoneEnabled: true,
isPromptNoneEnabledClientIds: [],
});
});
it('exists and extends OAuthWebIntegration', () => {
expect(model).toBeDefined();
expect(model instanceof OAuthWebIntegration).toBe(true);
});
describe('isSync', () => {
it('returns true for Firefox desktop client when service is sync', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
model.data.modelData.set('service', 'sync');
expect(model.isSync()).toBe(true);
});
it('returns true for Firefox desktop client when service is not defined', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
expect(model.isSync()).toBe(true);
});
it('returns true for Firefox iOS client', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxIOS);
expect(model.isSync()).toBe(true);
});
it('returns true for Fenix client', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.Fenix);
expect(model.isSync()).toBe(true);
});
it('returns true for Fennec client', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.Fennec);
expect(model.isSync()).toBe(true);
});
it('returns false for non-Sync services', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
model.data.modelData.set('service', 'relay');
expect(model.isSync()).toBe(false);
});
});
describe('isDesktopSync', () => {
it('returns true when client is Firefox desktop and service is sync', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
model.data.modelData.set('service', 'sync');
expect(model.isDesktopSync()).toBe(true);
});
it('returns false for non-sync service', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
model.data.modelData.set('service', 'relay');
expect(model.isDesktopSync()).toBe(false);
});
});
describe('isFirefoxMobileClient', () => {
it('returns true for Firefox iOS client ID', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxIOS);
expect(model.isFirefoxMobileClient()).toBe(true);
});
it('returns true for Fenix client ID', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.Fenix);
expect(model.isFirefoxMobileClient()).toBe(true);
});
it('returns true for Fennec client ID', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.Fennec);
expect(model.isFirefoxMobileClient()).toBe(true);
});
it('returns false for unknown client ID', () => {
model.clientInfo = mockClientInfo('unknown-client-id');
expect(model.isFirefoxMobileClient()).toBe(false);
});
});
describe('isFirefoxDesktopClient', () => {
it('returns true for Firefox desktop client ID', () => {
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
expect(model.isFirefoxDesktopClient()).toBe(true);
});
it('returns false for other client IDs', () => {
expect(model.isFirefoxDesktopClient()).toBe(false);
});
});
describe('wantsKeys', () => {
it('returns true', () => {
expect(model.wantsKeys()).toBe(true);
});
});
describe('serviceName', () => {
it('returns "Firefox" for non-sync services', () => {
model.data.modelData.set('service', 'non-sync-service');
expect(model.serviceName).toBe('Firefox');
});
it('returns Sync service name for sync service', () => {
model.data.modelData.set('service', 'sync');
expect(model.serviceName).toBe('Firefox Sync');
});
});
});

Просмотреть файл

@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 { Constants } from '../../lib/constants';
import { ModelDataStore } from '../../lib/model-data';
import { Integration, IntegrationType } from './base-integration';
import {
OAuthIntegrationOptions,
OAuthWebIntegration,
} from './oauth-web-integration';
export function isOAuthNativeIntegration(integration: {
type: IntegrationType;
}): integration is OAuthNativeIntegration {
return (
(integration as OAuthNativeIntegration).type === IntegrationType.OAuthNative
);
}
export type OAuthIntegration = OAuthWebIntegration | OAuthNativeIntegration;
export function isOAuthIntegration(integration: {
type: IntegrationType;
}): integration is OAuthIntegration {
return (
(integration as OAuthWebIntegration).type === IntegrationType.OAuthWeb ||
(integration as OAuthNativeIntegration).type === IntegrationType.OAuthNative
);
}
export enum OAuthNativeClients {
FirefoxIOS = '1b1a3e44c54fbb58',
FirefoxDesktop = '5882386c6d801776',
Fenix = 'a2270f727f45f648',
Fennec = '3332a18d142636cb',
}
/**
* A convenience function for the OAuthNativeIntegration type guard + isSync().
*/
export const isOAuthNativeIntegrationSync = (
integration: Pick<Integration, 'type'>
) => isOAuthNativeIntegration(integration) && integration.isSync();
/**
* This integration is used for OAuth implementations by the browser including
* mobile clients (currently all Sync), the oauth desktop sync flow, and the oauth
* desktop flow for other services.
*
* FxA sends and receives web channel messages if this integration is created.
*/
export class OAuthNativeIntegration extends OAuthWebIntegration {
constructor(
data: ModelDataStore,
protected readonly storageData: ModelDataStore,
public readonly opts: OAuthIntegrationOptions
) {
super(data, storageData, opts, IntegrationType.OAuthNative);
}
isSync() {
// For now, all mobile clients are Sync. This may change in the future,
// in which case we'll want a similar check to `isSyncDesktop`.
return this.isDesktopSync() || this.isFirefoxMobileClient();
}
isDesktopSync() {
return (
this.isFirefoxDesktopClient() &&
// Sync oauth desktop should always provide a `service=sync` parameter but
// we'll also default to Sync if it's missing.
(this.data.service === undefined || this.data.service === 'sync')
);
}
isFirefoxMobileClient() {
return (
this.clientInfo?.clientId === OAuthNativeClients.FirefoxIOS ||
this.clientInfo?.clientId === OAuthNativeClients.Fenix ||
this.clientInfo?.clientId === OAuthNativeClients.Fennec
);
}
isFirefoxDesktopClient() {
return this.clientInfo?.clientId === OAuthNativeClients.FirefoxDesktop;
}
wantsKeys() {
return true;
}
// TODO in FXA-10313, check for "Relay" or whatever makes sense at implementation
get serviceName() {
if (this.data.service === 'sync') {
return Constants.RELIER_SYNC_SERVICE_NAME;
} else {
return 'Firefox';
}
}
}

Просмотреть файл

@ -3,12 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ModelDataStore, GenericData } from '../../lib/model-data';
import { OAuthIntegration, replaceItemInArray } from './oauth-integration';
import {
OAuthWebIntegration,
replaceItemInArray,
} from './oauth-web-integration';
describe('models/integrations/oauth-relier', function () {
let data: ModelDataStore;
let oauthData: ModelDataStore;
let model: OAuthIntegration;
let model: OAuthWebIntegration;
beforeEach(function () {
data = new GenericData({
@ -17,7 +20,7 @@ describe('models/integrations/oauth-relier', function () {
oauthData = new GenericData({
scope: 'profile',
});
model = new OAuthIntegration(data, oauthData, {
model = new OAuthWebIntegration(data, oauthData, {
scopedKeysEnabled: true,
scopedKeysValidation: {},
isPromptNoneEnabled: true,
@ -39,7 +42,7 @@ describe('models/integrations/oauth-relier', function () {
const SCOPE_WITH_OPENID = 'profile:email profile:uid openid';
function getIntegrationWithScope(scope: string) {
const integration = new OAuthIntegration(
const integration = new OAuthWebIntegration(
new GenericData({
scope,
}),
@ -170,6 +173,13 @@ describe('models/integrations/oauth-relier', function () {
});
});
describe('getService', () => {
it('returns clientId as service', () => {
model.data.modelData.set('client_id', '123');
expect(model.getService()).toBe('123');
});
});
describe('replaceItemInArray', () => {
it('handles empty array', () => {
expect(replaceItemInArray([], 'foo', ['bar'])).toEqual([]);

Просмотреть файл

@ -4,8 +4,6 @@
import {
BaseIntegration,
Integration,
IntegrationFeatures,
IntegrationType,
RelierAccount,
RelierClientInfo,
@ -29,10 +27,6 @@ import {
} from 'class-validator';
import { AuthUiError } from '../../lib/auth-errors/auth-errors';
export interface OAuthIntegrationFeatures extends IntegrationFeatures {
webChannelSupport: boolean;
}
export enum OAuthPrompt {
CONSENT = 'consent',
NONE = 'none',
@ -40,25 +34,13 @@ export enum OAuthPrompt {
}
type OAuthIntegrationTypes =
| IntegrationType.OAuth
| IntegrationType.OAuthWeb
| IntegrationType.OAuthNative
| IntegrationType.PairingSupplicant
| IntegrationType.PairingAuthority;
export type SearchParam = IntegrationFlags['searchParam'];
export function isOAuthIntegration(integration: {
type: IntegrationType;
}): integration is OAuthIntegration {
return (integration as OAuthIntegration).type === IntegrationType.OAuth;
}
/**
* Sync mobile or sync desktop with context=oauth_webchannel_v1 (FF 123+)
*/
export const isSyncOAuthIntegration = (
integration: Pick<Integration, 'type'>
) => isOAuthIntegration(integration) && integration.isSync();
// TODO: probably move this somewhere else
export class OAuthIntegrationData extends BaseIntegrationData {
// TODO - Validation - Can we get a set of known client ids from config or api call? See https://github.com/mozilla/fxa/pull/15677#discussion_r1291534277
@ -180,12 +162,18 @@ export type OAuthIntegrationOptions = {
isPromptNoneEnabledClientIds: Array<string>;
};
export class OAuthIntegration extends BaseIntegration<OAuthIntegrationFeatures> {
/**
* This integration is used for relying party OAuth implementations. FxA should
* not send or receive web channel messages if this integration is created.
*
* This is a base class for OAuthNativeIntegration.
*/
export class OAuthWebIntegration extends BaseIntegration {
constructor(
data: ModelDataStore,
protected readonly storageData: ModelDataStore,
public readonly opts: OAuthIntegrationOptions,
type: OAuthIntegrationTypes = IntegrationType.OAuth
type: OAuthIntegrationTypes = IntegrationType.OAuthWeb
) {
super(type, new OAuthIntegrationData(data));
this.setFeatures({
@ -233,9 +221,8 @@ export class OAuthIntegration extends BaseIntegration<OAuthIntegrationFeatures>
return this.getRedirectToRPUrl({ error: err.errno });
}
// prefer client id if available (for oauth) otherwise fallback to service (e.g. for sync)
getService() {
return this.data.clientId || this.data.service;
return this.data.clientId;
}
restoreOAuthState() {
@ -293,10 +280,6 @@ export class OAuthIntegration extends BaseIntegration<OAuthIntegrationFeatures>
return this.clientInfo;
}
isSync() {
return this.data.context === Constants.OAUTH_WEBCHANNEL_CONTEXT;
}
isTrusted() {
return this.clientInfo?.trusted === true;
}
@ -323,9 +306,6 @@ export class OAuthIntegration extends BaseIntegration<OAuthIntegrationFeatures>
}
wantsKeys(): boolean {
if (this.isSync()) {
return true;
}
if (!this.opts.scopedKeysEnabled) {
return false;
}

Просмотреть файл

@ -5,10 +5,10 @@
import { ModelDataStore } from '../../lib/model-data';
import { IntegrationType } from './base-integration';
import {
OAuthIntegration,
OAuthWebIntegration,
OAuthIntegrationData,
OAuthIntegrationOptions,
} from './oauth-integration';
} from './oauth-web-integration';
import { bind, KeyTransforms as T } from '../../lib/model-data';
import { IsBase64, IsNotEmpty } from 'class-validator';
@ -24,7 +24,7 @@ export class PairingAuthorityIntegrationData extends OAuthIntegrationData {
//
// Also keep in mind, in content-server:
// Authority auth_broker extends from Base auth_broker and Authority relier extends from OAuthRelier
export class PairingAuthorityIntegration extends OAuthIntegration {
export class PairingAuthorityIntegration extends OAuthWebIntegration {
constructor(
data: ModelDataStore,
protected readonly storageData: ModelDataStore,

Просмотреть файл

@ -5,7 +5,10 @@ import { ModelDataStore } from '../../lib/model-data';
import { OAuthIntegrationData } from '.';
import { IntegrationType } from './base-integration';
import { bind } from '../../lib/model-data';
import { OAuthIntegration, OAuthIntegrationOptions } from './oauth-integration';
import {
OAuthWebIntegration,
OAuthIntegrationOptions,
} from './oauth-web-integration';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
// TODO in the 'Pairing' React epic. This shouldn't have any `feature` overrides but feel
@ -19,7 +22,7 @@ export class PairingSupplicantIntegrationData extends OAuthIntegrationData {
scope: string | undefined = '';
}
export class PairingSupplicantIntegration extends OAuthIntegration {
export class PairingSupplicantIntegration extends OAuthWebIntegration {
constructor(
data: ModelDataStore,
protected readonly storageData: ModelDataStore,

Просмотреть файл

@ -70,6 +70,9 @@ type SyncIntegrationTypes =
* via webchannels. Currently it is only used 1) when a user is on a verification page
* through Sync in a different browser, which will no longer be the case once we use
* codes for reset PW, and 2) as a base class for sync desktop v3.
*
* TODO in FXA-10313, let's just get rid of this now that we're on codes.
* Move methods into SyncDesktopV3Integration.
*/
export class SyncBasicIntegration<
T extends SyncIntegrationFeatures

Просмотреть файл

@ -10,7 +10,7 @@ import {
} from './sync-basic-integration';
/**
* Sync desktop with context=fx_desktop_v3 (FF < 123)
* Sync desktop with context=fx_desktop_v3
*/
export function isSyncDesktopV3Integration(integration: {
type: IntegrationType;
@ -21,12 +21,22 @@ export function isSyncDesktopV3Integration(integration: {
);
}
/* This is a legacy integration for desktop Firefox < 123 that must be supported
/* This is a legacy integration for desktop Firefox that must be supported
* for the foreseeable future.
*
* FxA sends and receives web channel messages if this integration is created.
*/
export class SyncDesktopV3Integration extends SyncBasicIntegration<SyncIntegrationFeatures> {
constructor(data: ModelDataStore) {
super(data, IntegrationType.SyncDesktopV3);
this.setFeatures({ allowUidChange: true });
}
isDesktopSync() {
return true;
}
isFirefoxDesktopClient() {
return true;
}
}

Просмотреть файл

@ -11,11 +11,11 @@ import InputText from '../../components/InputText';
import { FtlMsg } from 'fxa-react/lib/utils';
import ThirdPartyAuth from '../../components/ThirdPartyAuth';
import TermsPrivacyAgreement from '../../components/TermsPrivacyAgreement';
import { isOAuthIntegration } from '../../models';
import {
isClientMonitor,
isClientPocket,
} from '../../models/integrations/client-matching';
import { isOAuthIntegration } from '../../models';
export const Index = ({
integration,

Просмотреть файл

@ -5,11 +5,28 @@
import React from 'react';
import { LocationProvider } from '@reach/router';
import { MozServices } from '../../lib/types';
import { IntegrationType } from '../../models';
import { IntegrationType, OAuthIntegration } from '../../models';
import { IndexIntegration } from './interfaces';
import Index from '.';
import { MOCK_CLIENT_ID } from '../mocks';
export function createMockIndexOAuthIntegration({
clientId = MOCK_CLIENT_ID,
}): IndexIntegration {
return {
type: IntegrationType.OAuthWeb,
isSync: () => false,
getService: () => clientId,
};
}
export function createMockIndexSyncIntegration(): IndexIntegration {
return {
type: IntegrationType.OAuthNative,
isSync: () => true,
getService: () => MOCK_CLIENT_ID,
};
}
export function createMockIndexWebIntegration(): IndexIntegration {
return {
type: IntegrationType.Web,
@ -18,24 +35,6 @@ export function createMockIndexWebIntegration(): IndexIntegration {
};
}
export function createMockIndexSyncIntegration(): IndexIntegration {
return {
type: IntegrationType.OAuth,
isSync: () => true,
getService: () => MOCK_CLIENT_ID,
};
}
export function createMockIndexOAuthIntegration({
clientId = MOCK_CLIENT_ID,
}): IndexIntegration {
return {
type: IntegrationType.OAuth,
isSync: () => false,
getService: () => clientId,
};
}
export const Subject = ({
integration = createMockIndexWebIntegration(),
serviceName = MozServices.Default,

Просмотреть файл

@ -4,9 +4,9 @@
import { Integration, IntegrationType } from '../../../models';
export const mockResetPasswordOAuthIntegration = () => {
export const mockResetPasswordOAuthNativeIntegration = () => {
const mockIntegration = {
type: IntegrationType.OAuth,
type: IntegrationType.OAuthNative,
getService: () => 'sync',
isSync: () => true,
wantsKeys: () => true,

Просмотреть файл

@ -24,6 +24,7 @@ export function createMockWebIntegration() {
getService: () => MozServices.Default,
isSync: () => false,
wantsKeys: () => false,
isDesktopSync: () => false,
data: {},
};
}

Просмотреть файл

@ -10,7 +10,6 @@ import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
import VerificationMethods from '../../../constants/verification-methods';
import {
Integration,
isOAuthIntegration,
useAuthClient,
useFtlMsgResolver,
useSensitiveDataClient,
@ -61,8 +60,7 @@ const SigninUnblockContainer = ({
const { email, hasLinkedAccount, hasPassword } = location.state || {};
const wantsTwoStepAuthentication =
isOAuthIntegration(integration) && integration.wantsTwoStepAuthentication();
const wantsTwoStepAuthentication = integration.wantsTwoStepAuthentication();
const { finishOAuthFlowHandler, oAuthDataError } = useFinishOAuthFlowHandler(
authClient,

Просмотреть файл

@ -72,17 +72,29 @@ function mockSyncDesktopV3Integration() {
isSync: () => true,
wantsKeys: () => true,
data: { service: 'sync' },
isDesktopSync: () => true,
} as Integration;
}
function mockSyncOAuthIntegration(
function mockOAuthWebIntegration(
{ data }: { data?: { service?: string } } = { data: { service: 'sync' } }
) {
integration = {
type: IntegrationType.OAuth,
type: IntegrationType.OAuthWeb,
getService: () => MozServices.Monitor,
isSync: () => false,
wantsKeys: () => true,
data,
isDesktopSync: () => false,
} as Integration;
}
function mockOAuthNativeIntegration() {
integration = {
type: IntegrationType.OAuthNative,
getService: () => 'sync',
isSync: () => true,
wantsKeys: () => true,
data,
isDesktopSync: () => true,
} as Integration;
}
@ -92,6 +104,7 @@ function mockWebIntegration() {
getService: () => MozServices.Default,
isSync: () => false,
wantsKeys: () => false,
isDesktopSync: () => false,
data: {},
} as Integration;
}
@ -623,8 +636,8 @@ describe('signin container', () => {
expect(firefox.fxaCanLinkAccount).not.toHaveBeenCalled();
});
});
it('is not called when conditions are not met (oauth integration)', async () => {
mockSyncOAuthIntegration({ data: {} });
it('is not called when conditions are not met (oauth web integration)', async () => {
mockOAuthWebIntegration({ data: {} });
(firefox.fxaCanLinkAccount as jest.Mock).mockImplementationOnce(
async () => ({
ok: true,
@ -640,8 +653,8 @@ describe('signin container', () => {
expect(firefox.fxaCanLinkAccount).not.toHaveBeenCalled();
});
});
it('calls fxaCanLinkAccount when conditions are met (oauth integration)', async () => {
mockSyncOAuthIntegration();
it('calls fxaCanLinkAccount when conditions are met (oauth native integration, isSyncDesktop)', async () => {
mockOAuthNativeIntegration();
(firefox.fxaCanLinkAccount as jest.Mock).mockImplementationOnce(
async () => ({
ok: true,

Просмотреть файл

@ -248,9 +248,7 @@ const SigninContainer = ({
// warning. The browser will automatically respond with { ok: true } without
// prompting the user if it matches the email the browser has data for.
if (
// NOTE, SYNC-4408 OAuth desktop needs to add `service=sync` as a query parameter
// for this to work for OAuth desktop
integration.data.service === 'sync' &&
integration.isDesktopSync() &&
queryParamModel.hasLinkedAccount === undefined
) {
const { ok } = await firefox.fxaCanLinkAccount({ email });
@ -306,11 +304,7 @@ const SigninContainer = ({
if (
'data' in result &&
result.data &&
// NOTE, Oauth desktop needs to add `service=sync` as a query parameter for this
// to take users to the inline recovery key flow (SYNC-4408). (We may want
// check for client ID to determine oauth desktop instead, TBD slight refactor for
// FXA-10313).
options.service === 'sync' &&
integration.isDesktopSync() &&
config.featureFlags?.recoveryCodeSetupOnSyncSignIn === true &&
localStorage.getItem(
Constants.DISABLE_PROMO_ACCOUNT_RECOVERY_KEY_DO_IT_LATER

Просмотреть файл

@ -37,7 +37,6 @@ import {
} from '../../models/integrations/client-matching';
import firefox from '../../lib/channels/firefox';
import { navigate } from '@reach/router';
import { sessionToken } from '../../lib/cache';
import { IntegrationType } from '../../models';
// import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';

Просмотреть файл

@ -23,10 +23,10 @@ import GleanMetrics from '../../lib/glean';
import { usePageViewEvent } from '../../lib/metrics';
import { StoredAccountData, storeAccountData } from '../../lib/storage-utils';
import {
isOAuthIntegration,
useSensitiveDataClient,
useFtlMsgResolver,
isWebIntegration,
isOAuthIntegration,
} from '../../models';
import {
isClientMonitor,

Просмотреть файл

@ -19,7 +19,10 @@ export interface AvatarResponse {
}
export type SigninIntegration =
| Pick<Integration, 'type' | 'isSync' | 'getService' | 'wantsKeys' | 'data'>
| Pick<
Integration,
'type' | 'isSync' | 'getService' | 'wantsKeys' | 'data' | 'isDesktopSync'
>
| SigninOAuthIntegration;
export type SigninOAuthIntegration = Pick<
@ -31,6 +34,7 @@ export type SigninOAuthIntegration = Pick<
| 'wantsKeys'
| 'wantsLogin'
| 'data'
| 'isDesktopSync'
>;
export interface LocationState {

Просмотреть файл

@ -104,11 +104,12 @@ export function createMockSigninWebIntegration(): SigninIntegration {
getService: () => MozServices.Default,
wantsKeys: () => false,
data: {},
isDesktopSync: () => false,
};
}
export function createMockSigninSyncIntegration(
type = IntegrationType.OAuth
type = IntegrationType.OAuthNative
): SigninIntegration {
return {
type,
@ -116,6 +117,7 @@ export function createMockSigninSyncIntegration(
wantsKeys: () => true,
getService: () => MozServices.FirefoxSync,
data: {},
isDesktopSync: () => true,
};
}
@ -129,12 +131,13 @@ export function createMockSigninOAuthIntegration({
isSync?: boolean;
} = {}): SigninOAuthIntegration {
return {
type: IntegrationType.OAuth,
type: IntegrationType.OAuthWeb,
getService: () => clientId || MOCK_CLIENT_ID,
isSync: () => isSync,
wantsKeys: () => wantsKeys,
wantsLogin: () => false,
wantsTwoStepAuthentication: () => false,
isDesktopSync: () => isSync,
data: {},
};
}

Просмотреть файл

@ -91,7 +91,7 @@ function applyMocks() {
jest.restoreAllMocks();
integration = {
type: ModelsModule.IntegrationType.OAuth,
type: ModelsModule.IntegrationType.OAuthWeb,
} as Integration;
jest
.spyOn(ConfirmSignupCodeModule, 'default')

Просмотреть файл

@ -15,10 +15,15 @@ import {
MOCK_AUTH_ERROR,
MOCK_SIGNUP_CODE,
Subject,
createMockOAuthIntegration,
createMockOAuthNativeIntegration,
createMockOAuthWebIntegration,
createMockWebIntegration,
} from './mocks';
import { MOCK_STORED_ACCOUNT } from '../../mocks';
import {
MOCK_OAUTH_FLOW_HANDLER_RESPONSE,
MOCK_STORED_ACCOUNT,
mockFinishOAuthFlowHandler,
} from '../../mocks';
import GleanMetrics from '../../../lib/glean';
import { useWebRedirect } from '../../../lib/hooks/useWebRedirect';
import { ConfirmSignupCodeIntegration } from './interfaces';
@ -28,6 +33,7 @@ import {
tryAgainError,
} from '../../../lib/oauth/hooks';
import { OAUTH_ERRORS } from '../../../lib/oauth';
import firefox from '../../../lib/channels/firefox';
jest.mock('../../../lib/metrics', () => ({
usePageViewEvent: jest.fn(),
@ -80,15 +86,27 @@ function renderWithSession({
newsletterSlugs,
integration,
finishOAuthFlowHandler,
offeredSyncEngines,
declinedSyncEngines,
}: {
session?: Session;
newsletterSlugs?: string[];
integration?: ConfirmSignupCodeIntegration;
finishOAuthFlowHandler?: FinishOAuthFlowHandler;
offeredSyncEngines?: string[];
declinedSyncEngines?: string[];
}) {
renderWithLocalizationProvider(
<AppContext.Provider value={mockAppContext({ session })}>
<Subject {...{ newsletterSlugs, integration, finishOAuthFlowHandler }} />
<Subject
{...{
newsletterSlugs,
integration,
finishOAuthFlowHandler,
offeredSyncEngines,
declinedSyncEngines,
}}
/>
</AppContext.Provider>
);
}
@ -205,8 +223,12 @@ describe('ConfirmSignupCode page', () => {
});
});
describe('OAuth integration', () => {
const integration = createMockOAuthIntegration();
describe('OAuth web integration', () => {
let fxaOAuthLoginSpy: jest.SpyInstance;
beforeEach(() => {
fxaOAuthLoginSpy = jest.spyOn(firefox, 'fxaOAuthLogin');
});
const integration = createMockOAuthWebIntegration();
it('shows an error banner for an OAuth error', async () => {
renderWithSession({
@ -220,6 +242,49 @@ describe('ConfirmSignupCode page', () => {
screen.getByText(OAUTH_ERRORS.TRY_AGAIN.message);
});
});
it('does not send web channel messages', async () => {
renderWithSession({
session,
integration,
finishOAuthFlowHandler: jest.fn(),
});
submit();
await waitFor(() => {
expect(fxaOAuthLoginSpy).not.toHaveBeenCalled();
});
});
});
describe('OAuth native integration', () => {
let fxaOAuthLoginSpy: jest.SpyInstance;
beforeEach(() => {
fxaOAuthLoginSpy = jest.spyOn(firefox, 'fxaOAuthLogin');
});
const integration = createMockOAuthNativeIntegration();
it('sends expected web channel messages', async () => {
const offeredSyncEngines = ['blabbitybee', 'bloopitybop'];
const declinedSyncEngines = ['bloopitybop'];
renderWithSession({
session,
integration,
finishOAuthFlowHandler: mockFinishOAuthFlowHandler,
declinedSyncEngines,
offeredSyncEngines,
});
submit();
await waitFor(() => {
expect(fxaOAuthLoginSpy).toHaveBeenCalledWith({
declinedSyncEngines,
offeredSyncEngines,
action: 'signup',
...MOCK_OAUTH_FLOW_HANDLER_RESPONSE,
});
});
});
});
describe('Web integration on submission', () => {

Просмотреть файл

@ -4,14 +4,7 @@
import { RouteComponentProps } from '@reach/router';
import { FinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
import {
BaseIntegration,
BaseIntegrationData,
Integration,
IntegrationType,
OAuthIntegration,
OAuthIntegrationData,
} from '../../../models';
import { Integration, OAuthWebIntegration } from '../../../models';
import { StoredAccountData } from '../../../lib/storage-utils';
import { QueryParams } from '../../..';
@ -49,33 +42,25 @@ export interface ConfirmSignupCodeFormData {
code: string;
}
export interface ConfirmSignupCodeBaseIntegration {
type: IntegrationType;
data: {
uid: BaseIntegrationData['uid'];
redirectTo: BaseIntegrationData['redirectTo'];
};
getService: BaseIntegration['getService'];
}
export type ConfirmSignupCodeBaseIntegration = Pick<
Integration,
'type' | 'data' | 'getService'
>;
export interface ConfirmSignupCodeOAuthIntegration {
type: IntegrationType.OAuth;
data: {
uid: OAuthIntegrationData['uid'];
redirectTo: OAuthIntegrationData['redirectTo'];
};
getService: () => ReturnType<OAuthIntegration['getService']>;
getRedirectUri: () => ReturnType<OAuthIntegration['getRedirectUri']>;
wantsTwoStepAuthentication: () => ReturnType<
OAuthIntegration['wantsTwoStepAuthentication']
>;
isSync: () => ReturnType<OAuthIntegration['isSync']>;
getPermissions: () => ReturnType<OAuthIntegration['getPermissions']>;
}
export type ConfirmSignupCodeOAuthIntegration = Pick<
OAuthWebIntegration,
| 'type'
| 'data'
| 'getService'
| 'getRedirectUri'
| 'wantsTwoStepAuthentication'
| 'isSync'
| 'getPermissions'
>;
export type ConfirmSignupCodeIntegration =
| ConfirmSignupCodeOAuthIntegration
| ConfirmSignupCodeBaseIntegration;
| ConfirmSignupCodeBaseIntegration
| ConfirmSignupCodeOAuthIntegration;
export interface GetEmailBounceStatusResponse {
emailBounceStatus: { hasHardBounce: boolean };

Просмотреть файл

@ -5,7 +5,7 @@
import React from 'react';
import { LocationProvider } from '@reach/router';
import ConfirmSignupCode from '.';
import { IntegrationType } from '../../../models';
import { IntegrationType, OAuthNativeClients } from '../../../models';
import {
MOCK_EMAIL,
MOCK_FLOW_ID,
@ -42,17 +42,29 @@ export function createMockWebIntegration({
};
}
export function createMockOAuthIntegration(
export function createMockOAuthWebIntegration(
serviceName = MOCK_SERVICE
): ConfirmSignupCodeOAuthIntegration {
return {
type: IntegrationType.OAuth,
type: IntegrationType.OAuthWeb,
data: { uid: MOCK_UID, redirectTo: undefined },
getRedirectUri: () => MOCK_REDIRECT_URI,
getService: () => serviceName,
wantsTwoStepAuthentication: () => false,
getPermissions: () => [],
isSync: () => false,
getPermissions: () => [],
};
}
export function createMockOAuthNativeIntegration(): ConfirmSignupCodeOAuthIntegration {
return {
type: IntegrationType.OAuthNative,
data: { uid: MOCK_UID, redirectTo: undefined },
getRedirectUri: () => MOCK_REDIRECT_URI,
getService: () => OAuthNativeClients.FirefoxDesktop,
wantsTwoStepAuthentication: () => false,
isSync: () => true,
getPermissions: () => [],
};
}
@ -60,10 +72,14 @@ export const Subject = ({
integration = createMockWebIntegration(),
newsletterSlugs,
finishOAuthFlowHandler = mockFinishOAuthFlowHandler,
offeredSyncEngines,
declinedSyncEngines,
}: {
integration?: ConfirmSignupCodeIntegration;
newsletterSlugs?: string[];
finishOAuthFlowHandler?: FinishOAuthFlowHandler;
offeredSyncEngines?: string[];
declinedSyncEngines?: string[];
}) => {
return (
<LocationProvider>
@ -72,6 +88,8 @@ export const Subject = ({
integration,
newsletterSlugs,
finishOAuthFlowHandler,
offeredSyncEngines,
declinedSyncEngines,
}}
flowQueryParams={{ flowId: MOCK_FLOW_ID }}
email={MOCK_EMAIL}

Просмотреть файл

@ -312,7 +312,7 @@ describe('sign-up-container', () => {
});
describe('web-channel-interactions', () => {
describe('sync desktop', () => {
describe('SyncDesktopV3 integration', () => {
beforeEach(() => {
// here we override some key behaviors to alter the containers behavior
serviceName = MozServices.FirefoxSync;
@ -338,12 +338,12 @@ describe('sign-up-container', () => {
});
});
describe('sync-service-on-oauth', () => {
describe('OAuth native integration with Sync', () => {
beforeEach(() => {
serviceName = MozServices.FirefoxSync;
integration.getService = () => MozServices.FirefoxSync;
integration.isSync = () => true;
integration.type = IntegrationType.OAuth;
integration.type = IntegrationType.OAuthNative;
});
it('adds event listeners and sends', async () => {
await render();
@ -359,10 +359,11 @@ describe('sign-up-container', () => {
});
});
describe('non-sync-service', () => {
describe('Web integration, default service', () => {
beforeEach(() => {
integration.type = IntegrationType.Web;
integration.getService = () => MozServices.Default;
integration.isSync = () => false;
});
it('did not add event listeners and send command', async () => {

Просмотреть файл

@ -6,7 +6,7 @@ import { RouteComponentProps, useLocation } from '@reach/router';
import { useNavigateWithQuery as useNavigate } from '../../lib/hooks/useNavigateWithQuery';
import {
Integration,
isOAuthIntegration,
isOAuthNativeIntegrationSync,
isSyncDesktopV3Integration,
useAuthClient,
useConfig,
@ -96,10 +96,9 @@ const SignupContainer = ({
string[] | undefined
>();
const isOAuth = isOAuthIntegration(integration);
const isSyncOAuth = isOAuth && integration.isSync();
const isSyncOAuth = isOAuthNativeIntegrationSync(integration);
const isSyncDesktopV3 = isSyncDesktopV3Integration(integration);
const isSyncWebChannel = isSyncOAuth || isSyncDesktopV3;
const isSync = integration.isSync();
const wantsKeys = integration.wantsKeys();
useEffect(() => {
@ -141,7 +140,7 @@ const SignupContainer = ({
// that we listen for.
// TODO: In content-server, we send this on app-start for all integration types.
// Do we want to move this somewhere else once the index page is Reactified?
if (isSyncWebChannel) {
if (isSync) {
(async () => {
const status = await firefox.fxaStatus({
// TODO: Improve getting 'context', probably set this on the integration
@ -154,6 +153,8 @@ const SignupContainer = ({
if (!webChannelEngines && status.capabilities.engines) {
// choose_what_to_sync may be disabled for mobile sync, see:
// https://github.com/mozilla/application-services/issues/1761
// Desktop OAuth Sync will always provide this capability too
// for consistency.
if (
isSyncDesktopV3 ||
(isSyncOAuth && status.capabilities.choose_what_to_sync)
@ -163,7 +164,7 @@ const SignupContainer = ({
}
})();
}
}, [isSyncWebChannel, isSyncDesktopV3, isSyncOAuth, webChannelEngines]);
}, [isSync, isSyncDesktopV3, isSyncOAuth, webChannelEngines]);
const [beginSignup] = useMutation<BeginSignupResponse>(BEGIN_SIGNUP_MUTATION);
@ -272,8 +273,6 @@ const SignupContainer = ({
queryParamModel,
beginSignupHandler,
webChannelEngines,
isSyncWebChannel,
isSyncOAuth,
}}
/>
);

Просмотреть файл

@ -8,7 +8,7 @@ import { LocationProvider } from '@reach/router';
import { Meta } from '@storybook/react';
import { withLocalization } from 'fxa-react/lib/storybooks';
import {
createMockSignupOAuthIntegration,
createMockSignupOAuthWebIntegration,
createMockSignupSyncDesktopV3Integration,
mockBeginSignupHandler,
signupQueryParams,
@ -21,10 +21,6 @@ import {
POCKET_CLIENTIDS,
} from '../../models/integrations/client-matching';
import { getSyncEngineIds } from '../../components/ChooseWhatToSync/sync-engines';
import {
isSyncOAuthIntegration,
isSyncDesktopV3Integration,
} from '../../models';
import { MOCK_CLIENT_ID } from '../mocks';
export default {
@ -37,10 +33,8 @@ const urlQueryData = mockUrlQueryData(signupQueryParams);
const queryParamModel = new SignupQueryParams(urlQueryData);
const storyWithProps = (
integration: SignupIntegration = createMockSignupOAuthIntegration()
integration: SignupIntegration = createMockSignupOAuthWebIntegration()
) => {
const isSyncOAuth = isSyncOAuthIntegration(integration);
const story = () => (
<LocationProvider>
<Signup
@ -49,9 +43,6 @@ const storyWithProps = (
queryParamModel,
beginSignupHandler: mockBeginSignupHandler,
webChannelEngines: getSyncEngineIds(),
isSyncWebChannel:
isSyncOAuth || isSyncDesktopV3Integration(integration),
isSyncOAuth,
}}
/>
</LocationProvider>
@ -64,11 +55,11 @@ export const Default = storyWithProps();
export const CantChangeEmail = storyWithProps();
export const ClientIsPocket = storyWithProps(
createMockSignupOAuthIntegration(POCKET_CLIENTIDS[0])
createMockSignupOAuthWebIntegration(POCKET_CLIENTIDS[0])
);
export const ClientIsMonitor = storyWithProps(
createMockSignupOAuthIntegration(MONITOR_CLIENTIDS[0])
createMockSignupOAuthWebIntegration(MONITOR_CLIENTIDS[0])
);
export const SyncDesktopV3 = storyWithProps(
@ -76,5 +67,5 @@ export const SyncDesktopV3 = storyWithProps(
);
export const SyncOAuth = storyWithProps(
createMockSignupOAuthIntegration(MOCK_CLIENT_ID, true)
createMockSignupOAuthWebIntegration(MOCK_CLIENT_ID)
);

Просмотреть файл

@ -20,11 +20,11 @@ import {
BEGIN_SIGNUP_HANDLER_RESPONSE,
MOCK_SEARCH_PARAMS,
Subject,
createMockSignupOAuthIntegration,
createMockSignupOAuthWebIntegration,
createMockSignupOAuthNativeIntegration,
createMockSignupSyncDesktopV3Integration,
} from './mocks';
import {
MOCK_CLIENT_ID,
MOCK_EMAIL,
MOCK_KEY_FETCH_TOKEN,
MOCK_PASSWORD,
@ -178,7 +178,7 @@ describe('Signup page', () => {
it('shows an info banner and Pocket-specific TOS when client is Pocket', async () => {
renderWithLocalizationProvider(
<Subject
integration={createMockSignupOAuthIntegration(POCKET_CLIENTIDS[0])}
integration={createMockSignupOAuthWebIntegration(POCKET_CLIENTIDS[0])}
/>
);
@ -590,11 +590,11 @@ describe('Signup page', () => {
});
});
describe('on success with Sync OAuth integration', () => {
describe('on success with OAuthNative integration with Sync', () => {
beforeEach(() => {
renderWithLocalizationProvider(
<Subject
integration={createMockSignupOAuthIntegration('', true)}
integration={createMockSignupOAuthNativeIntegration()}
beginSignupHandler={mockBeginSignupHandler}
/>
);
@ -711,14 +711,14 @@ describe('Signup page', () => {
});
});
it('on success with OAuth integration', async () => {
it('on success with OAuth Web integration', async () => {
const mockBeginSignupHandler = jest
.fn()
.mockResolvedValue(BEGIN_SIGNUP_HANDLER_RESPONSE);
renderWithLocalizationProvider(
<Subject
integration={createMockSignupOAuthIntegration(MOCK_CLIENT_ID)}
integration={createMockSignupOAuthWebIntegration()}
beginSignupHandler={mockBeginSignupHandler}
/>
);

Просмотреть файл

@ -41,6 +41,7 @@ import { StoredAccountData, storeAccountData } from '../../lib/storage-utils';
import { MozServices } from '../../lib/types';
import {
isOAuthIntegration,
isOAuthNativeIntegrationSync,
isSyncDesktopV3Integration,
useFtlMsgResolver,
} from '../../models';
@ -57,8 +58,6 @@ export const Signup = ({
queryParamModel,
beginSignupHandler,
webChannelEngines,
isSyncWebChannel,
isSyncOAuth,
}: SignupProps) => {
usePageViewEvent(viewName, REACT_ENTRYPOINT);
@ -66,6 +65,8 @@ export const Signup = ({
GleanMetrics.registration.view();
}, []);
const isSyncOAuth = isOAuthNativeIntegrationSync(integration);
const isSyncDesktopV3 = isSyncDesktopV3Integration(integration);
const isSync = integration.isSync();
const email = queryParamModel.email;
@ -111,7 +112,7 @@ export const Signup = ({
useEffect(() => {
if (webChannelEngines) {
if (isSyncDesktopV3Integration(integration)) {
if (isSyncDesktopV3) {
// Desktop v3 web channel message sends additional engines
setOfferedSyncEngineConfigs([
...defaultDesktopV3SyncEngineConfigs,
@ -119,7 +120,7 @@ export const Signup = ({
webChannelEngines.includes(engine.id)
),
]);
} else if (isSyncWebChannel) {
} else if (isSyncOAuth) {
// OAuth Webchannel context sends all engines
setOfferedSyncEngineConfigs(
syncEngineConfigs.filter((engine) =>
@ -128,7 +129,7 @@ export const Signup = ({
);
}
}
}, [integration, isSyncWebChannel, webChannelEngines]);
}, [isSyncDesktopV3, isSyncOAuth, webChannelEngines]);
useEffect(() => {
if (offeredSyncEngineConfigs) {
@ -244,7 +245,7 @@ export const Signup = ({
const getOfferedSyncEngines = () =>
getSyncEngineIds(offeredSyncEngineConfigs || []);
if (integration.isSync()) {
if (isSync) {
const syncEngines = {
offeredEngines: getOfferedSyncEngines(),
declinedEngines: declinedSyncEngines,
@ -315,7 +316,7 @@ export const Signup = ({
selectedNewsletterSlugs,
declinedSyncEngines,
email,
integration,
isSync,
offeredSyncEngineConfigs,
isSyncOAuth,
localizedValidAgeError,
@ -323,7 +324,7 @@ export const Signup = ({
);
const showCWTS = () => {
if (isSyncWebChannel) {
if (isSync) {
if (offeredSyncEngineConfigs) {
return (
<ChooseWhatToSync

Просмотреть файл

@ -47,14 +47,12 @@ export interface SignupProps {
queryParamModel: SignupQueryParams;
beginSignupHandler: BeginSignupHandler;
webChannelEngines: string[] | undefined;
isSyncWebChannel: boolean;
isSyncOAuth: boolean;
}
export type SignupIntegration = SignupOAuthIntegration | SignupBaseIntegration;
export interface SignupOAuthIntegration {
type: IntegrationType.OAuth;
type: IntegrationType.OAuthWeb | IntegrationType.OAuthNative;
isSync: () => ReturnType<OAuthIntegration['isSync']>;
getRedirectUri: () => ReturnType<OAuthIntegration['getRedirectUri']>;
saveOAuthState: () => ReturnType<OAuthIntegration['saveOAuthState']>;

Просмотреть файл

@ -5,11 +5,7 @@
import { LocationProvider } from '@reach/router';
import Signup from '.';
import { MozServices } from '../../lib/types';
import {
IntegrationType,
isSyncDesktopV3Integration,
isSyncOAuthIntegration,
} from '../../models';
import { IntegrationType } from '../../models';
import { mockUrlQueryData } from '../../models/mocks';
import { SignupQueryParams } from '../../models/pages/signup';
import {
@ -50,12 +46,24 @@ export function createMockSignupSyncDesktopV3Integration(): SignupBaseIntegratio
};
}
export function createMockSignupOAuthIntegration(
clientId?: string,
isSync = false
export function createMockSignupOAuthWebIntegration(
clientId?: string
): SignupOAuthIntegration {
return {
type: IntegrationType.OAuth,
type: IntegrationType.OAuthWeb,
getRedirectUri: () => MOCK_REDIRECT_URI,
saveOAuthState: () => {},
getService: () => clientId || MOCK_CLIENT_ID,
isSync: () => false,
};
}
export function createMockSignupOAuthNativeIntegration(
clientId?: string,
isSync = true
): SignupOAuthIntegration {
return {
type: IntegrationType.OAuthNative,
getRedirectUri: () => MOCK_REDIRECT_URI,
saveOAuthState: () => {},
getService: () => clientId || MOCK_CLIENT_ID,
@ -118,7 +126,6 @@ export const Subject = ({
}) => {
const urlQueryData = mockUrlQueryData(queryParams);
const queryParamModel = new SignupQueryParams(urlQueryData);
const isSyncOAuth = isSyncOAuthIntegration(integration);
return (
<LocationProvider>
<Signup
@ -126,9 +133,6 @@ export const Subject = ({
integration,
queryParamModel,
beginSignupHandler,
isSyncOAuth,
isSyncWebChannel:
isSyncOAuth || isSyncDesktopV3Integration(integration),
webChannelEngines: getSyncEngineIds(),
}}
/>