Skip to content

Commit 75d4329

Browse files
authored
Merge pull request #18498 from mozilla/FXA-8636
feat(react): Reactify email-first/Index page
2 parents 3d56ce5 + ea02587 commit 75d4329

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1448
-502
lines changed

packages/functional-tests/pages/signin.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ export class SigninPage extends BaseLayout {
104104

105105
get syncSignInHeading() {
106106
return this.page.getByRole('heading', {
107-
name: /^Continue to your Mozilla account/,
107+
// Fluent inserts directional markers around "Mozilla account" so
108+
// just look for partial match
109+
name: /^Continue to your/,
108110
});
109111
}
110112

packages/functional-tests/pages/signup.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export class SignupPage extends BaseLayout {
1111

1212
get emailFormHeading() {
1313
return this.page.getByRole('heading', {
14-
name: /^Enter your email|^Continue to your Mozilla account/,
14+
// Fluent inserts directional markers around "Mozilla account" so
15+
// just look for partial match
16+
name: /^Enter your email|^Continue to your/,
1517
});
1618
}
1719

packages/functional-tests/tests/key-stretching-v2/signinBlocked.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,6 @@ async function removeAccount(
9797
await settings.deleteAccountButton.click();
9898
await deleteAccount.deleteAccount(password);
9999

100-
await expect(page).toHaveURL(`${target.baseUrl}?delete_account_success=true`);
100+
await expect(page).toHaveURL(`${target.baseUrl}`);
101101
await expect(page.getByText('Account deleted successfully')).toBeVisible();
102102
}

packages/functional-tests/tests/oauth/loginHint.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ test.describe('severity-2 #smoke', () => {
2525
}) => {
2626
const { email } = testAccountTracker.generateAccountDetails();
2727

28-
await relier.goto(`login_hint=${email}`);
28+
await relier.goto(
29+
`login_hint=${email}&forceExperiment=generalizedReactApp&forceExperimentGroup=react`
30+
);
2931
await relier.clickEmailFirst();
3032

3133
await page.waitForURL(`${target.contentServerUrl}/oauth/signup**`);
34+
3235
await expect(signup.signupFormHeading).toBeVisible();
3336
// email provided as login hint is displayed on the signup page
3437
await expect(page.getByText(email)).toBeVisible();

packages/fxa-content-server/app/scripts/lib/router.js

+21-13
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ Cocktail.mixin(Router, ReactExperimentMixin);
115115

116116
Router = Router.extend({
117117
routes: {
118-
'(/)': createViewHandler(IndexView),
118+
'(/)': function () {
119+
this.createReactOrBackboneViewHandler('/', IndexView);
120+
},
119121
'account_recovery_confirm_key(/)': function () {
120122
this.createReactOrBackboneViewHandler(
121123
'account_recovery_confirm_key',
@@ -692,18 +694,24 @@ Router = Router.extend({
692694
},
693695

694696
createReactViewHandler(routeName, additionalParams) {
695-
const { deviceId, flowBeginTime, flowId } =
696-
this.metrics.getFlowEventMetadata();
697-
698-
const link = `/${routeName}${Url.objToSearchString({
699-
showReactApp: true,
700-
deviceId,
701-
flowBeginTime,
702-
flowId,
703-
...additionalParams,
704-
})}`;
705-
706-
this.navigateAway(link);
697+
if (routeName === '/') {
698+
const queryParams = new URLSearchParams(this.window.location.search);
699+
queryParams.set('showReactApp', 'true');
700+
this.navigateAway(`/?${queryParams.toString()}`);
701+
} else {
702+
const { deviceId, flowBeginTime, flowId } =
703+
this.metrics.getFlowEventMetadata();
704+
705+
const link = `/${routeName}${Url.objToSearchString({
706+
showReactApp: true,
707+
deviceId,
708+
flowBeginTime,
709+
flowId,
710+
...additionalParams,
711+
})}`;
712+
713+
this.navigateAway(link);
714+
}
707715
},
708716

709717
createReactOrBackboneViewHandler(

packages/fxa-content-server/server/bin/fxa-content-server.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,6 @@ function makeApp() {
191191
const routeHelpers = routing(app, routeLogger);
192192

193193
function addNonSettingsRoutes(middleware) {
194-
addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n);
195-
196194
/* This creates `app.whatever('/path' ...` handlers for every content-server route and
197195
* excludes routes in `react-app.js` if corresponding feature flags are on. We manually add
198196
* these excluded routes for content-server to serve in checks above if the feature flag is
@@ -201,6 +199,11 @@ function makeApp() {
201199
* route implementations. */
202200
routes.forEach(routeHelpers.addRoute);
203201

202+
// Adding React routes should come _after_ adding Backbone routes above because the Index
203+
// page ('/'), which will get added to Backbone routing too in this function if conditions
204+
// are not met for React, must come _after_ at least some of the frontend Backbone routing.
205+
addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n, config);
206+
204207
// must come after route handling but before wildcard routes
205208
app.use(
206209
serveStatic(STATIC_DIRECTORY, {

packages/fxa-content-server/server/config/local.json-dist

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"resetPasswordRoutes": true,
6363
"signUpRoutes": true,
6464
"signInRoutes": true,
65-
"emailFirstRoutes": false,
65+
"emailFirstRoutes": true,
6666
"postVerifyThirdPartyAuthRoutes": true
6767
},
6868
"featureFlags": {

packages/fxa-content-server/server/lib/beta-settings.js

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const settingsConfig = {
9090
showReactApp: {
9191
signUpRoutes: config.get('showReactApp.signUpRoutes'),
9292
signInRoutes: config.get('showReactApp.signInRoutes'),
93+
emailFirstRoutes: config.get('showReactApp.emailFirstRoutes'),
9394
},
9495
rolloutRates: {
9596
keyStretchV2: config.get('rolloutRates.keyStretchV2'),

packages/fxa-content-server/server/lib/routes.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ module.exports = function (config, i18n, statsd, glean) {
1212
const redirectVersionedToUnversioned = require('./routes/redirect-versioned-to-unversioned');
1313
const reactRouteGroups = getServerReactRouteGroups(
1414
config.get('showReactApp'),
15-
i18n
15+
i18n,
16+
config
1617
);
1718

1819
const routes = [
@@ -25,7 +26,7 @@ module.exports = function (config, i18n, statsd, glean) {
2526
require('./routes/get-oauth-success').default(reactRouteGroups),
2627
require('./routes/get-terms-privacy').default(reactRouteGroups, i18n),
2728
require('./routes/get-update-firefox')(config),
28-
require('./routes/get-index')(config),
29+
require('./routes/get-index').default(reactRouteGroups, config),
2930
require('./routes/get-ver.json'),
3031
require('./routes/get-client.json')(i18n),
3132
require('./routes/get-config')(i18n),

packages/fxa-content-server/server/lib/routes/get-index.js

+18-184
Original file line numberDiff line numberDiff line change
@@ -4,190 +4,24 @@
44

55
'use strict';
66

7-
const flowMetrics = require('../flow-metrics');
8-
const logger = require('../logging/log')('routes.index');
9-
10-
module.exports = function (config) {
11-
let featureFlags;
12-
const featureFlagConfig = config.get('featureFlags');
13-
if (featureFlagConfig.enabled) {
14-
featureFlags = require('fxa-shared').featureFlags(
15-
featureFlagConfig,
16-
logger
7+
const {
8+
getIndexRouteDefinition,
9+
} = require('./react-app/route-definition-index');
10+
11+
/**
12+
* Remove index route ('/') from list if React feature flag is set to true
13+
* and route is included in the emailFirstRoutes route group.
14+
*/
15+
/** @type {import("./react-app/types").GetBackboneRouteDefinition} */
16+
function getIndex(reactRouteGroups, config) {
17+
const isOnReact =
18+
reactRouteGroups.emailFirstRoutes.featureFlagOn &&
19+
reactRouteGroups.emailFirstRoutes.routes.find(
20+
(route) => route.name === '/'
1721
);
18-
} else {
19-
featureFlags = { get: () => ({}) };
20-
}
21-
22-
const AUTH_SERVER_URL = config.get('fxaccount_url');
23-
const CLIENT_ID = config.get('oauth_client_id');
24-
const COPPA_ENABLED = config.get('coppa.enabled');
25-
const ENV = config.get('env');
26-
const FLOW_ID_KEY = config.get('flow_id_key');
27-
const MARKETING_EMAIL_ENABLED = config.get('marketing_email.enabled');
28-
const MARKETING_EMAIL_PREFERENCES_URL = config.get(
29-
'marketing_email.preferences_url'
30-
);
31-
const MX_RECORD_VALIDATION = config.get('mxRecordValidation');
32-
const MAX_EVENT_OFFSET = config.get('client_metrics.max_event_offset');
33-
const REDIRECT_CHECK_ALLOW_LIST = config.get('redirect_check.allow_list');
34-
const SENTRY_CLIENT_DSN = config.get('sentry.dsn');
35-
const SENTRY_CLIENT_ENV = config.get('sentry.env');
36-
const SENTRY_SAMPLE_RATE = config.get('sentry.sampleRate');
37-
const SENTRY_TRACES_SAMPLE_RATE = config.get('sentry.tracesSampleRate');
38-
const SENTRY_CLIENT_NAME = config.get('sentry.clientName');
39-
const OAUTH_SERVER_URL = config.get('oauth_url');
40-
const PAIRING_CHANNEL_URI = config.get('pairing.server_base_uri');
41-
const PAIRING_CLIENTS = config.get('pairing.clients');
42-
const PROFILE_SERVER_URL = config.get('profile_url');
43-
const STATIC_RESOURCE_URL = config.get('static_resource_url');
44-
const SCOPED_KEYS_ENABLED = config.get('scopedKeys.enabled');
45-
const SCOPED_KEYS_VALIDATION = config.get('scopedKeys.validation');
46-
const SUBSCRIPTIONS = config.get('subscriptions');
47-
const GOOGLE_AUTH_CONFIG = config.get('googleAuthConfig');
48-
const APPLE_AUTH_CONFIG = config.get('appleAuthConfig');
49-
const PROMPT_NONE_ENABLED = config.get('oauth.prompt_none.enabled');
50-
const SHOW_REACT_APP = config.get('showReactApp');
51-
const BRAND_MESSAGING_MODE = config.get('brandMessagingMode');
52-
const FEATURE_FLAGS_FXA_STATUS_ON_SETTINGS = config.get(
53-
'featureFlags.sendFxAStatusOnSettings'
54-
);
55-
const FEATURE_FLAGS_RECOVERY_CODE_SETUP_ON_SYNC_SIGN_IN = config.get(
56-
'featureFlags.recoveryCodeSetupOnSyncSignIn'
57-
);
58-
const FEATURE_FLAGS_ENABLE_ADDING_2FA_BACKUP_PHONE = config.get(
59-
'featureFlags.enableAdding2FABackupPhone'
60-
);
61-
const FEATURE_FLAGS_ENABLE_USING_2FA_BACKUP_PHONE = config.get(
62-
'featureFlags.enableUsing2FABackupPhone'
63-
);
64-
const GLEAN_ENABLED = config.get('glean.enabled');
65-
const GLEAN_APPLICATION_ID = config.get('glean.applicationId');
66-
const GLEAN_UPLOAD_ENABLED = config.get('glean.uploadEnabled');
67-
const GLEAN_APP_CHANNEL = config.get('glean.appChannel');
68-
const GLEAN_SERVER_ENDPOINT = config.get('glean.serverEndpoint');
69-
const GLEAN_LOG_PINGS = config.get('glean.logPings');
70-
const GLEAN_DEBUG_VIEW_TAG = config.get('glean.debugViewTag');
71-
72-
// Rather than relay all rollout rates, hand pick the ones that are applicable
73-
const ROLLOUT_RATES = config.get('rolloutRates');
74-
75-
// Note that this list is only enforced for clients that use login_hint/email
76-
// with prompt=none. id_token_hint clients are not subject to this check.
77-
const PROMPT_NONE_ENABLED_CLIENT_IDS = new Set(
78-
config.get('oauth.prompt_none.enabled_client_ids')
79-
);
80-
81-
// add version from package.json to config
82-
const RELEASE = require('../../../package.json').version;
83-
const WEBPACK_PUBLIC_PATH = `${STATIC_RESOURCE_URL}/${config.get(
84-
'jsResourcePath'
85-
)}/`;
86-
87-
const configForFrontEnd = {
88-
authServerUrl: AUTH_SERVER_URL,
89-
maxEventOffset: MAX_EVENT_OFFSET,
90-
env: ENV,
91-
isCoppaEnabled: COPPA_ENABLED,
92-
isPromptNoneEnabled: PROMPT_NONE_ENABLED,
93-
googleAuthConfig: GOOGLE_AUTH_CONFIG,
94-
appleAuthConfig: APPLE_AUTH_CONFIG,
95-
marketingEmailEnabled: MARKETING_EMAIL_ENABLED,
96-
marketingEmailPreferencesUrl: MARKETING_EMAIL_PREFERENCES_URL,
97-
mxRecordValidation: MX_RECORD_VALIDATION,
98-
oAuthClientId: CLIENT_ID,
99-
oAuthUrl: OAUTH_SERVER_URL,
100-
pairingChannelServerUri: PAIRING_CHANNEL_URI,
101-
pairingClients: PAIRING_CLIENTS,
102-
profileUrl: PROFILE_SERVER_URL,
103-
release: RELEASE,
104-
redirectAllowlist: REDIRECT_CHECK_ALLOW_LIST,
105-
rolloutRates: ROLLOUT_RATES,
106-
scopedKeysEnabled: SCOPED_KEYS_ENABLED,
107-
scopedKeysValidation: SCOPED_KEYS_VALIDATION,
108-
sentry: {
109-
dsn: SENTRY_CLIENT_DSN,
110-
env: SENTRY_CLIENT_ENV,
111-
sampleRate: SENTRY_SAMPLE_RATE,
112-
clientName: SENTRY_CLIENT_NAME,
113-
tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,
114-
},
115-
staticResourceUrl: STATIC_RESOURCE_URL,
116-
subscriptions: SUBSCRIPTIONS,
117-
webpackPublicPath: WEBPACK_PUBLIC_PATH,
118-
showReactApp: SHOW_REACT_APP,
119-
brandMessagingMode: BRAND_MESSAGING_MODE,
120-
featureFlags: {
121-
sendFxAStatusOnSettings: FEATURE_FLAGS_FXA_STATUS_ON_SETTINGS,
122-
recoveryCodeSetupOnSyncSignIn:
123-
FEATURE_FLAGS_RECOVERY_CODE_SETUP_ON_SYNC_SIGN_IN,
124-
enableAdding2FABackupPhone: FEATURE_FLAGS_ENABLE_ADDING_2FA_BACKUP_PHONE,
125-
enableUsing2FABackupPhone: FEATURE_FLAGS_ENABLE_USING_2FA_BACKUP_PHONE,
126-
},
127-
glean: {
128-
// feature toggle
129-
enabled: GLEAN_ENABLED,
130-
// Glean SDK config
131-
applicationId: GLEAN_APPLICATION_ID,
132-
uploadEnabled: GLEAN_UPLOAD_ENABLED,
133-
appDisplayVersion: RELEASE,
134-
channel: GLEAN_APP_CHANNEL,
135-
serverEndpoint: GLEAN_SERVER_ENDPOINT,
136-
logPings: GLEAN_LOG_PINGS,
137-
debugViewTag: GLEAN_DEBUG_VIEW_TAG,
138-
},
139-
};
140-
141-
const NO_LONGER_SUPPORTED_CONTEXTS = new Set([
142-
'fx_desktop_v1',
143-
'fx_desktop_v2',
144-
'fx_firstrun_v2',
145-
'iframe',
146-
]);
147-
148-
return {
149-
method: 'get',
150-
path: '/',
151-
process: async function (req, res) {
152-
const flowEventData = flowMetrics.create(FLOW_ID_KEY);
153-
154-
if (NO_LONGER_SUPPORTED_CONTEXTS.has(req.query.context)) {
155-
return res.redirect(`/update_firefox?${req.originalUrl.split('?')[1]}`);
156-
}
157-
158-
let flags;
159-
try {
160-
flags = await featureFlags.get();
161-
} catch (err) {
162-
logger.error('featureFlags.error', err);
163-
flags = {};
164-
}
165-
166-
const isPromptNoneEnabledForClient =
167-
req.query.client_id &&
168-
PROMPT_NONE_ENABLED_CLIENT_IDS.has(req.query.client_id);
169-
170-
res.render('index', {
171-
// Note that bundlePath is added to templates as a build step
172-
bundlePath: '/bundle',
173-
config: encodeURIComponent(
174-
JSON.stringify({
175-
...configForFrontEnd,
176-
isPromptNoneEnabled: PROMPT_NONE_ENABLED,
177-
isPromptNoneEnabledForClient,
178-
})
179-
),
180-
featureFlags: encodeURIComponent(JSON.stringify(flags)),
181-
flowBeginTime: flowEventData.flowBeginTime,
182-
flowId: flowEventData.flowId,
183-
// Note that staticResourceUrl is added to templates as a build step
184-
staticResourceUrl: STATIC_RESOURCE_URL,
185-
});
22+
return isOnReact ? null : getIndexRouteDefinition(config);
23+
}
18624

187-
if (req.headers.dnt === '1') {
188-
logger.info('request.headers.dnt');
189-
}
190-
},
191-
terminate: featureFlags.terminate,
192-
};
25+
module.exports = {
26+
default: getIndex,
19327
};

0 commit comments

Comments
 (0)