Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): Reactify email-first/Index page #18498

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions packages/fxa-content-server/app/scripts/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ Cocktail.mixin(Router, ReactExperimentMixin);

Router = Router.extend({
routes: {
'(/)': createViewHandler(IndexView),
'(/)': function () {
this.createReactOrBackboneViewHandler('/', IndexView);
},
'account_recovery_confirm_key(/)': function () {
this.createReactOrBackboneViewHandler(
'account_recovery_confirm_key',
Expand Down Expand Up @@ -692,18 +694,22 @@ Router = Router.extend({
},

createReactViewHandler(routeName, additionalParams) {
const { deviceId, flowBeginTime, flowId } =
this.metrics.getFlowEventMetadata();

const link = `/${routeName}${Url.objToSearchString({
showReactApp: true,
deviceId,
flowBeginTime,
flowId,
...additionalParams,
})}`;

this.navigateAway(link);
if (routeName === '/') {
this.navigateAway('/?showReactApp=true');
} else {
const { deviceId, flowBeginTime, flowId } =
this.metrics.getFlowEventMetadata();

const link = `/${routeName}${Url.objToSearchString({
showReactApp: true,
deviceId,
flowBeginTime,
flowId,
...additionalParams,
})}`;

this.navigateAway(link);
}
},

createReactOrBackboneViewHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,6 @@ function makeApp() {
const routeHelpers = routing(app, routeLogger);

function addNonSettingsRoutes(middleware) {
addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n);

/* This creates `app.whatever('/path' ...` handlers for every content-server route and
* excludes routes in `react-app.js` if corresponding feature flags are on. We manually add
* these excluded routes for content-server to serve in checks above if the feature flag is
Expand All @@ -201,6 +199,11 @@ function makeApp() {
* route implementations. */
routes.forEach(routeHelpers.addRoute);

// Adding React routes should come _after_ adding Backbone routes above because the Index
// page ('/'), which will get added to Backbone routing too in this function if conditions
// are not met for React, must come _after_ at least some of the frontend Backbone routing.
addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n, config);

// must come after route handling but before wildcard routes
app.use(
serveStatic(STATIC_DIRECTORY, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"resetPasswordRoutes": true,
"signUpRoutes": true,
"signInRoutes": true,
"emailFirstRoutes": false,
"emailFirstRoutes": true,
"postVerifyThirdPartyAuthRoutes": true
},
"featureFlags": {
Expand Down
1 change: 1 addition & 0 deletions packages/fxa-content-server/server/lib/beta-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const settingsConfig = {
showReactApp: {
signUpRoutes: config.get('showReactApp.signUpRoutes'),
signInRoutes: config.get('showReactApp.signInRoutes'),
emailFirstRoutes: config.get('showReactApp.emailFirstRoutes'),
},
rolloutRates: {
keyStretchV2: config.get('rolloutRates.keyStretchV2'),
Expand Down
5 changes: 3 additions & 2 deletions packages/fxa-content-server/server/lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ module.exports = function (config, i18n, statsd, glean) {
const redirectVersionedToUnversioned = require('./routes/redirect-versioned-to-unversioned');
const reactRouteGroups = getServerReactRouteGroups(
config.get('showReactApp'),
i18n
i18n,
config
);

const routes = [
Expand All @@ -25,7 +26,7 @@ module.exports = function (config, i18n, statsd, glean) {
require('./routes/get-oauth-success').default(reactRouteGroups),
require('./routes/get-terms-privacy').default(reactRouteGroups, i18n),
require('./routes/get-update-firefox')(config),
require('./routes/get-index')(config),
require('./routes/get-index').default(reactRouteGroups, config),
require('./routes/get-ver.json'),
require('./routes/get-client.json')(i18n),
require('./routes/get-config')(i18n),
Expand Down
202 changes: 18 additions & 184 deletions packages/fxa-content-server/server/lib/routes/get-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,190 +4,24 @@

'use strict';

const flowMetrics = require('../flow-metrics');
const logger = require('../logging/log')('routes.index');

module.exports = function (config) {
let featureFlags;
const featureFlagConfig = config.get('featureFlags');
if (featureFlagConfig.enabled) {
featureFlags = require('fxa-shared').featureFlags(
featureFlagConfig,
logger
const {
getIndexRouteDefinition,
} = require('./react-app/route-definition-index');

/**
* Remove index route ('/') from list if React feature flag is set to true
* and route is included in the emailFirstRoutes route group.
*/
/** @type {import("./react-app/types").GetBackboneRouteDefinition} */
function getIndex(reactRouteGroups, config) {
const isOnReact =
reactRouteGroups.emailFirstRoutes.featureFlagOn &&
reactRouteGroups.emailFirstRoutes.routes.find(
(route) => route.name === '/'
);
} else {
featureFlags = { get: () => ({}) };
}

const AUTH_SERVER_URL = config.get('fxaccount_url');
const CLIENT_ID = config.get('oauth_client_id');
const COPPA_ENABLED = config.get('coppa.enabled');
const ENV = config.get('env');
const FLOW_ID_KEY = config.get('flow_id_key');
const MARKETING_EMAIL_ENABLED = config.get('marketing_email.enabled');
const MARKETING_EMAIL_PREFERENCES_URL = config.get(
'marketing_email.preferences_url'
);
const MX_RECORD_VALIDATION = config.get('mxRecordValidation');
const MAX_EVENT_OFFSET = config.get('client_metrics.max_event_offset');
const REDIRECT_CHECK_ALLOW_LIST = config.get('redirect_check.allow_list');
const SENTRY_CLIENT_DSN = config.get('sentry.dsn');
const SENTRY_CLIENT_ENV = config.get('sentry.env');
const SENTRY_SAMPLE_RATE = config.get('sentry.sampleRate');
const SENTRY_TRACES_SAMPLE_RATE = config.get('sentry.tracesSampleRate');
const SENTRY_CLIENT_NAME = config.get('sentry.clientName');
const OAUTH_SERVER_URL = config.get('oauth_url');
const PAIRING_CHANNEL_URI = config.get('pairing.server_base_uri');
const PAIRING_CLIENTS = config.get('pairing.clients');
const PROFILE_SERVER_URL = config.get('profile_url');
const STATIC_RESOURCE_URL = config.get('static_resource_url');
const SCOPED_KEYS_ENABLED = config.get('scopedKeys.enabled');
const SCOPED_KEYS_VALIDATION = config.get('scopedKeys.validation');
const SUBSCRIPTIONS = config.get('subscriptions');
const GOOGLE_AUTH_CONFIG = config.get('googleAuthConfig');
const APPLE_AUTH_CONFIG = config.get('appleAuthConfig');
const PROMPT_NONE_ENABLED = config.get('oauth.prompt_none.enabled');
const SHOW_REACT_APP = config.get('showReactApp');
const BRAND_MESSAGING_MODE = config.get('brandMessagingMode');
const FEATURE_FLAGS_FXA_STATUS_ON_SETTINGS = config.get(
'featureFlags.sendFxAStatusOnSettings'
);
const FEATURE_FLAGS_RECOVERY_CODE_SETUP_ON_SYNC_SIGN_IN = config.get(
'featureFlags.recoveryCodeSetupOnSyncSignIn'
);
const FEATURE_FLAGS_ENABLE_ADDING_2FA_BACKUP_PHONE = config.get(
'featureFlags.enableAdding2FABackupPhone'
);
const FEATURE_FLAGS_ENABLE_USING_2FA_BACKUP_PHONE = config.get(
'featureFlags.enableUsing2FABackupPhone'
);
const GLEAN_ENABLED = config.get('glean.enabled');
const GLEAN_APPLICATION_ID = config.get('glean.applicationId');
const GLEAN_UPLOAD_ENABLED = config.get('glean.uploadEnabled');
const GLEAN_APP_CHANNEL = config.get('glean.appChannel');
const GLEAN_SERVER_ENDPOINT = config.get('glean.serverEndpoint');
const GLEAN_LOG_PINGS = config.get('glean.logPings');
const GLEAN_DEBUG_VIEW_TAG = config.get('glean.debugViewTag');

// Rather than relay all rollout rates, hand pick the ones that are applicable
const ROLLOUT_RATES = config.get('rolloutRates');

// Note that this list is only enforced for clients that use login_hint/email
// with prompt=none. id_token_hint clients are not subject to this check.
const PROMPT_NONE_ENABLED_CLIENT_IDS = new Set(
config.get('oauth.prompt_none.enabled_client_ids')
);

// add version from package.json to config
const RELEASE = require('../../../package.json').version;
const WEBPACK_PUBLIC_PATH = `${STATIC_RESOURCE_URL}/${config.get(
'jsResourcePath'
)}/`;

const configForFrontEnd = {
authServerUrl: AUTH_SERVER_URL,
maxEventOffset: MAX_EVENT_OFFSET,
env: ENV,
isCoppaEnabled: COPPA_ENABLED,
isPromptNoneEnabled: PROMPT_NONE_ENABLED,
googleAuthConfig: GOOGLE_AUTH_CONFIG,
appleAuthConfig: APPLE_AUTH_CONFIG,
marketingEmailEnabled: MARKETING_EMAIL_ENABLED,
marketingEmailPreferencesUrl: MARKETING_EMAIL_PREFERENCES_URL,
mxRecordValidation: MX_RECORD_VALIDATION,
oAuthClientId: CLIENT_ID,
oAuthUrl: OAUTH_SERVER_URL,
pairingChannelServerUri: PAIRING_CHANNEL_URI,
pairingClients: PAIRING_CLIENTS,
profileUrl: PROFILE_SERVER_URL,
release: RELEASE,
redirectAllowlist: REDIRECT_CHECK_ALLOW_LIST,
rolloutRates: ROLLOUT_RATES,
scopedKeysEnabled: SCOPED_KEYS_ENABLED,
scopedKeysValidation: SCOPED_KEYS_VALIDATION,
sentry: {
dsn: SENTRY_CLIENT_DSN,
env: SENTRY_CLIENT_ENV,
sampleRate: SENTRY_SAMPLE_RATE,
clientName: SENTRY_CLIENT_NAME,
tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,
},
staticResourceUrl: STATIC_RESOURCE_URL,
subscriptions: SUBSCRIPTIONS,
webpackPublicPath: WEBPACK_PUBLIC_PATH,
showReactApp: SHOW_REACT_APP,
brandMessagingMode: BRAND_MESSAGING_MODE,
featureFlags: {
sendFxAStatusOnSettings: FEATURE_FLAGS_FXA_STATUS_ON_SETTINGS,
recoveryCodeSetupOnSyncSignIn:
FEATURE_FLAGS_RECOVERY_CODE_SETUP_ON_SYNC_SIGN_IN,
enableAdding2FABackupPhone: FEATURE_FLAGS_ENABLE_ADDING_2FA_BACKUP_PHONE,
enableUsing2FABackupPhone: FEATURE_FLAGS_ENABLE_USING_2FA_BACKUP_PHONE,
},
glean: {
// feature toggle
enabled: GLEAN_ENABLED,
// Glean SDK config
applicationId: GLEAN_APPLICATION_ID,
uploadEnabled: GLEAN_UPLOAD_ENABLED,
appDisplayVersion: RELEASE,
channel: GLEAN_APP_CHANNEL,
serverEndpoint: GLEAN_SERVER_ENDPOINT,
logPings: GLEAN_LOG_PINGS,
debugViewTag: GLEAN_DEBUG_VIEW_TAG,
},
};

const NO_LONGER_SUPPORTED_CONTEXTS = new Set([
'fx_desktop_v1',
'fx_desktop_v2',
'fx_firstrun_v2',
'iframe',
]);

return {
method: 'get',
path: '/',
process: async function (req, res) {
const flowEventData = flowMetrics.create(FLOW_ID_KEY);

if (NO_LONGER_SUPPORTED_CONTEXTS.has(req.query.context)) {
return res.redirect(`/update_firefox?${req.originalUrl.split('?')[1]}`);
}

let flags;
try {
flags = await featureFlags.get();
} catch (err) {
logger.error('featureFlags.error', err);
flags = {};
}

const isPromptNoneEnabledForClient =
req.query.client_id &&
PROMPT_NONE_ENABLED_CLIENT_IDS.has(req.query.client_id);

res.render('index', {
// Note that bundlePath is added to templates as a build step
bundlePath: '/bundle',
config: encodeURIComponent(
JSON.stringify({
...configForFrontEnd,
isPromptNoneEnabled: PROMPT_NONE_ENABLED,
isPromptNoneEnabledForClient,
})
),
featureFlags: encodeURIComponent(JSON.stringify(flags)),
flowBeginTime: flowEventData.flowBeginTime,
flowId: flowEventData.flowId,
// Note that staticResourceUrl is added to templates as a build step
staticResourceUrl: STATIC_RESOURCE_URL,
});
return isOnReact ? null : getIndexRouteDefinition(config);
}

if (req.headers.dnt === '1') {
logger.info('request.headers.dnt');
}
},
terminate: featureFlags.terminate,
};
module.exports = {
default: getIndex,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const { getServerReactRouteGroups } = require('./route-groups-server');
const config = require('../../configuration');

/** Add all routes from all route objects for fxa-settings or fxa-content-server to serve.
* @type {import("./types").AddRoutes}
*/
function addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n) {
/** Check if the feature flag passed in is `true` and the request contains `?showReactApp=true`.
* If true, use the middleware passed ('createSettingsProxy' in dev, else 'modifySettingsStatic')
* for that route, allowing `fxa-settings` to serve the page. If false, skip the middleware and
* use the default routing middleware from `fxa-shared/express/routing.ts`.
function addAllReactRoutesConditionally(
app,
routeHelpers,
middleware,
i18n,
config
) {
/** Check if the feature flag passed in is `true`, and either the request contains
* `?showReactApp=true` or `fullProdRollout` is set to `true`. If true, use the
* middleware passed ('createSettingsProxy' in dev, else 'modifySettingsStatic') for
* that route, allowing `fxa-settings` to serve the page. If false, skip the middleware
* and use the default routing middleware from `fxa-shared/express/routing.ts` to
* serve the Backbone app.
* @param {import("./types").ReactRouteGroup}
*/
function addReactRoutesConditionally({
Expand All @@ -25,11 +32,21 @@ function addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n) {
// possible TODO - `definition.method`s will either be 'get' or 'post'. Not sure if we need
// this for any 'post' requests but shouldn't hurt anything; 'get' alone may suffice.
app[definition.method](definition.path, (req, res, next) => {
if (req.query.showReactApp === 'true' || fullProdRollout === true) {
if (req.query.showReactApp === 'true' || fullProdRollout) {
// req.path === '/' seems to match some content-server routes like `/authorization`.
// To be sure we are only pointing to React for '/' with '?showReactApp=true' and not
// other routes when only '/' is set in react-app/index.js, explicitely check for
// `originalUrl` as well.
if (definition.path === '/') {
if (req.originalUrl.split('?')[0] === '/') {
return middleware(req, res, next);
} else {
return next('route');
}
}
return middleware(req, res, next);
} else {
next('route');
}
next('route');
});
// Manually add route for content-server to serve; occurs when above next('route'); is called
routeHelpers.addRoute(definition);
Expand All @@ -39,7 +56,8 @@ function addAllReactRoutesConditionally(app, routeHelpers, middleware, i18n) {

const reactRouteGroups = getServerReactRouteGroups(
config.get('showReactApp'),
i18n
i18n,
config
);
for (const routeGroup in reactRouteGroups) {
addReactRoutesConditionally(reactRouteGroups[routeGroup]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const getReactRouteGroups = (showReactApp, reactRoute) => {
return {
emailFirstRoutes: {
featureFlagOn: showReactApp.emailFirstRoutes,
routes: reactRoute.getRoutes([]),
routes: reactRoute.getRoutes(['/']),
fullProdRollout: false,
},
simpleRoutes: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const reactRouteClient = {
getRoute(name) {
if (
typeof name === 'string' &&
(FRONTEND_ROUTES.includes(name) ||
(name === '/' ||
FRONTEND_ROUTES.includes(name) ||
PAIRING_ROUTES.includes(name) ||
OAUTH_SUCCESS_ROUTES.includes(name))
) {
Expand Down
Loading