Skip to content

Commit 47309f6

Browse files
vpomerleauLZoog
authored andcommitted
feat(settings): Rollout new account recovery flow at 15%
Because: * We want to rollout the new account recovery key creation flow to 15% of users before rolling out to 100% on prod. This commit: * Add NewRecoveryKeyUI experiment with 15% rollout * Passes experiment group to Settings App, allows forceExperiment * Conditionally render new UI if user is in treatment group AND feature flag is turned on * Update related tests Closes #FXA-8030 Co-authored-by: Lauren Zugai <[email protected]>
1 parent 0d6f1d0 commit 47309f6

File tree

20 files changed

+170
-42
lines changed

20 files changed

+170
-42
lines changed

packages/functional-tests/pages/layout.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ export abstract class BaseLayout {
1414
return `${this.baseUrl}/${this.path}`;
1515
}
1616

17-
goto(waitUntil: 'networkidle' | 'domcontentloaded' | 'load' = 'load') {
18-
return this.page.goto(this.url, { waitUntil });
17+
goto(
18+
waitUntil: 'networkidle' | 'domcontentloaded' | 'load' = 'load',
19+
query?: string
20+
) {
21+
const url = query ? `${this.url}?${query}` : this.url;
22+
return this.page.goto(url, { waitUntil });
1923
}
2024

2125
screenshot() {

packages/functional-tests/pages/settings/layout.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export abstract class SettingsLayout extends BaseLayout {
99
return this.page.locator('[data-testid=drop-down-avatar-menu]');
1010
}
1111

12-
goto() {
13-
return super.goto('load');
12+
goto(query?: string) {
13+
return super.goto('load', query);
1414
}
1515

1616
async alertBarText() {

packages/functional-tests/tests/react-conversion/oauthResetPassword.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ async function addAccountRecoveryKeyFlow({
179179
credentials,
180180
pages: { settings, recoveryKey },
181181
}) {
182-
await settings.goto();
182+
await settings.goto('isInRecoveryKeyExperiment=true');
183183
await settings.recoveryKey.clickCreate();
184184
await recoveryKey.clickStart();
185185
await recoveryKey.setPassword(credentials.password);

packages/functional-tests/tests/react-conversion/recoveryKey.spec.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ test.describe('recovery key react', () => {
2424
await settings.goto();
2525
let status = await settings.recoveryKey.statusText();
2626
expect(status).toEqual('Not Set');
27-
await settings.recoveryKey.clickCreate();
2827

2928
// Check which account recovery key generation flow to use (based on feature flag)
3029
// TODO in FXA-7419 - remove the condition and else block that goes through the old key generation flow
3130
if (config.featureFlags.showRecoveryKeyV2 === true) {
31+
await settings.goto('isInRecoveryKeyExperiment=true');
32+
await settings.recoveryKey.clickCreate();
3233
// View 1/4 info
3334
await recoveryKey.clickStart();
3435
// View 2/4 confirm password and generate key
@@ -46,6 +47,7 @@ test.describe('recovery key react', () => {
4647
await recoveryKey.setHint(hint);
4748
await recoveryKey.clickFinish();
4849
} else {
50+
await settings.recoveryKey.clickCreate();
4951
await recoveryKey.setPassword(credentials.password);
5052
await recoveryKey.submit();
5153

@@ -91,7 +93,7 @@ test.describe('recovery key react', () => {
9193
await resetPasswordReact.fillEmailToResetPwd(credentials.email);
9294
await resetPasswordReact.confirmResetPasswordHeadingVisible();
9395

94-
// We need to append `&showReactApp=true` to reset link inorder to enroll in reset password experiment
96+
// We need to append `&showReactApp=true` to reset link in order to enroll in reset password experiment
9597
let link = await target.email.waitForEmail(
9698
credentials.email,
9799
EmailType.recovery,

packages/functional-tests/tests/settings/password.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ test.describe('severity-1 #smoke', () => {
4444
// Create recovery key
4545
// TODO in FXA-7419 - remove condition and only keep new recovery key flow (remove content of else block)
4646
if (config.featureFlags.showRecoveryKeyV2 === true) {
47-
await settings.goto();
47+
await settings.goto('isInRecoveryKeyExperiment=true');
4848

4949
await settings.recoveryKey.clickCreate();
5050
// View 1/4 info

packages/functional-tests/tests/settings/recoveryKey.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ test.describe('new recovery key test', () => {
231231
const config = await login.getConfig();
232232
test.skip(config.featureFlags.showRecoveryKeyV2 !== true);
233233

234-
await settings.goto();
234+
await settings.goto('isInRecoveryKeyExperiment=true');
235235
let status = await settings.recoveryKey.statusText();
236236
expect(status).toEqual('Not Set');
237237
await settings.recoveryKey.clickCreate();
@@ -355,6 +355,7 @@ test.describe('new recovery key test', () => {
355355
page,
356356
pages: { settings, recoveryKey, login },
357357
}) => {
358+
await settings.goto('isInRecoveryKeyExperiment=true');
358359
// Create new recovery key
359360
await settings.recoveryKey.clickCreate();
360361
// View 1/4 info
@@ -510,6 +511,7 @@ test.describe('new recovery key test', () => {
510511
});
511512

512513
test('revoke recovery key', async ({ pages: { settings } }) => {
514+
await settings.goto('isInRecoveryKeyExperiment=true');
513515
await settings.recoveryKey.clickDelete();
514516
await settings.clickModalConfirm();
515517
await settings.waitForAlertBar();

packages/fxa-content-server/app/scripts/lib/experiments/grouping-rules/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const experimentGroupingRules = [
1717
require('./push'),
1818
require('./third-party-auth'),
1919
require('./generalized-react-app'),
20+
require('./new-recovery-key-UI'),
2021
].map((ExperimentGroupingRule) => new ExperimentGroupingRule());
2122

2223
class ExperimentChoiceIndex {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
/**
6+
* Feature flag for new account recovery key UI.
7+
*
8+
*/
9+
'use strict';
10+
11+
const BaseGroupingRule = require('./base');
12+
13+
const GROUPS = [
14+
'control',
15+
// Treatment branch is the new account recovery key creation flow
16+
'treatment',
17+
];
18+
19+
// This experiment is disabled by default. If you would like to go through
20+
// the flow, load email-first screen and append query params
21+
// `?forceExperiment=newRecoveryKeyUI&forceExperimentGroup=treatment`
22+
const ROLLOUT_RATE = 0.15;
23+
24+
module.exports = class NewRecoveryKeyUI extends BaseGroupingRule {
25+
constructor() {
26+
super();
27+
this.name = 'newRecoveryKeyUI';
28+
this.groups = GROUPS;
29+
this.rolloutRate = ROLLOUT_RATE;
30+
}
31+
32+
/**
33+
* Enable new recovery key creation flow if user is in the treatment group.
34+
*
35+
* @param {Object} subject data used to decide
36+
* @returns {Any}
37+
*/
38+
choose(subject = {}) {
39+
let choice = false;
40+
41+
if (this.bernoulliTrial(this.rolloutRate, subject.uniqueUserId)) {
42+
choice = this.uniformChoice(GROUPS, subject.uniqueUserId);
43+
}
44+
45+
return choice;
46+
}
47+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import ExperimentMixin from '../views/mixins/experiment-mixin';
6+
7+
export default {
8+
dependsOn: [ExperimentMixin],
9+
10+
isInNewRecoveryKeyUIExperiment() {
11+
const experimentGroup =
12+
this.getAndReportExperimentGroup('newRecoveryKeyUI');
13+
return experimentGroup === 'treatment';
14+
},
15+
};

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import VerificationReasons from './verification-reasons';
4545
import WouldYouLikeToSync from '../views/would_you_like_to_sync';
4646
import { isAllowed } from 'fxa-shared/configuration/convict-format-allow-list';
4747
import ReactExperimentMixin from './generalized-react-app-experiment-mixin';
48+
import RecoveryKeyExperimentMixin from './new-recovery-key-UI-experiment-mixin';
4849
import { getClientReactRouteGroups } from '../../../server/lib/routes/react-app/route-groups-client';
4950

5051
const NAVIGATE_AWAY_IN_MOBILE_DELAY_MS = 75;
@@ -120,7 +121,7 @@ let Router = Backbone.Router.extend({
120121
},
121122
});
122123

123-
Cocktail.mixin(Router, ReactExperimentMixin);
124+
Cocktail.mixin(Router, ReactExperimentMixin, RecoveryKeyExperimentMixin);
124125

125126
Router = Router.extend({
126127
routes: {
@@ -389,6 +390,8 @@ Router = Router.extend({
389390
return this.navigateAway(redirectUrl);
390391
}
391392

393+
const isInRecoveryKeyExperiment = this.isInNewRecoveryKeyUIExperiment();
394+
392395
// All other flows should redirect to the settings page
393396
const settingsEndpoint = '/settings';
394397
const settingsLink = `${settingsEndpoint}${Url.objToSearchString({
@@ -400,6 +403,7 @@ Router = Router.extend({
400403
isSampledUser,
401404
service,
402405
uniqueUserId,
406+
...(isInRecoveryKeyExperiment && { isInRecoveryKeyExperiment }),
403407
})}`;
404408
this.navigateAway(settingsLink);
405409
},

packages/fxa-content-server/app/tests/spec/lib/experiments/grouping-rules/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import sinon from 'sinon';
99

1010
describe('lib/experiments/grouping-rules/index', () => {
1111
it('EXPERIMENT_NAMES is exported', () => {
12-
assert.lengthOf(ExperimentGroupingRules.EXPERIMENT_NAMES, 6);
12+
assert.lengthOf(ExperimentGroupingRules.EXPERIMENT_NAMES, 7);
1313
});
1414

1515
describe('choose', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { assert } from 'chai';
6+
import Experiment from 'lib/experiments/grouping-rules/new-recovery-key-UI';
7+
8+
describe('lib/experiments/grouping-rules/new-recovery-key-UI', () => {
9+
let experiment;
10+
11+
beforeEach(() => {
12+
experiment = new Experiment();
13+
});
14+
15+
describe('choose', () => {
16+
it('returns treatment if valid clientId', () => {
17+
const rules = experiment.groups;
18+
assert.isTrue(
19+
rules.include(
20+
experiment.choose({
21+
experimentGroupingRules: { choose: () => experiment.name },
22+
uniqueUserId: 'user-id',
23+
})
24+
)
25+
);
26+
});
27+
28+
it('returns false if rollout 0%', () => {
29+
experiment.rolloutRate = 0;
30+
assert.isFalse(
31+
experiment.choose({
32+
experimentGroupingRules: { choose: () => experiment.name },
33+
uniqueUserId: 'user-id',
34+
})
35+
);
36+
});
37+
});
38+
});

packages/fxa-settings/src/components/App/index.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const App = ({
5151
}: { flowQueryParams: QueryParams } & RouteComponentProps) => {
5252
const [isSignedIn, setIsSignedIn] = useState<boolean>();
5353

54-
const { showReactApp } = flowQueryParams;
54+
const { showReactApp, isInRecoveryKeyExperiment } = flowQueryParams;
5555
const { loading, error } = useInitialState();
5656
const account = useAccount();
5757
const [email, setEmail] = useState<string>();
@@ -63,6 +63,11 @@ export const App = ({
6363

6464
const { metricsEnabled } = account;
6565

66+
// TODO Remove feature flag and experiment logic in FXA-7419
67+
const showRecoveryKeyV2 = !!(
68+
config.showRecoveryKeyV2 && isInRecoveryKeyExperiment === 'true'
69+
);
70+
6671
useEffect(() => {
6772
Metrics.init(metricsEnabled || !isSignedIn, flowQueryParams);
6873
if (metricsEnabled) {
@@ -227,7 +232,7 @@ export const App = ({
227232
<ThirdPartyAuthCallback path="/post_verify/third_party_auth/callback/*" />
228233
</>
229234
)}
230-
<Settings path="/settings/*" />
235+
<Settings path="/settings/*" {...{ showRecoveryKeyV2 }} />
231236
</ScrollToTop>
232237
</Router>
233238
</>

packages/fxa-settings/src/components/Settings/PageRecoveryKeyCreate/index.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import React, { useState } from 'react';
6-
import { RouteComponentProps, useNavigate } from '@reach/router';
6+
import { RouteComponentProps, useLocation, useNavigate } from '@reach/router';
77
import { HomePath } from '../../../constants';
88
import { usePageViewEvent } from '../../../lib/metrics';
99
import { useAccount, useFtlMsgResolver } from '../../../models';
@@ -23,6 +23,7 @@ export enum RecoveryKeyAction {
2323

2424
export const PageRecoveryKeyCreate = (props: RouteComponentProps) => {
2525
usePageViewEvent(viewName);
26+
const location = useLocation();
2627

2728
const { recoveryKey } = useAccount();
2829
const ftlMsgResolver = useFtlMsgResolver();
@@ -48,7 +49,7 @@ export const PageRecoveryKeyCreate = (props: RouteComponentProps) => {
4849

4950
// TODO: Remove feature flag param in FXA-7419
5051
const navigateBackward = () => {
51-
navigate(HomePath);
52+
navigate(`${HomePath}${location.search}`);
5253
};
5354

5455
const navigateForward = (e?: React.MouseEvent<HTMLElement>) => {
@@ -57,7 +58,7 @@ export const PageRecoveryKeyCreate = (props: RouteComponentProps) => {
5758
setCurrentStep(currentStep + 1);
5859
} else {
5960
// TODO: Remove feature flag param in FXA-7419
60-
navigate(HomePath);
61+
navigate(`${HomePath}${location.search}`);
6162
}
6263
};
6364

packages/fxa-settings/src/components/Settings/PageSettings/index.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import { DeleteAccountPath } from 'fxa-settings/src/constants';
1616
import { Localized } from '@fluent/react';
1717
import DataCollection from '../DataCollection';
1818

19-
export const PageSettings = (_: RouteComponentProps) => {
19+
export const PageSettings = ({
20+
// This should technically never be `undefined` but because this is temporary,
21+
// allowing `undefined` makes updating tests and stories easier.
22+
showRecoveryKeyV2,
23+
}: { showRecoveryKeyV2?: boolean } & RouteComponentProps) => {
2024
const { uid } = useAccount();
2125

2226
Metrics.setProperties({
@@ -32,7 +36,7 @@ export const PageSettings = (_: RouteComponentProps) => {
3236
</div>
3337
<div className="flex-7 max-w-full">
3438
<Profile />
35-
<Security />
39+
<Security {...{ showRecoveryKeyV2 }} />
3640
<ConnectedServices />
3741
<LinkedAccounts />
3842
<DataCollection />

packages/fxa-settings/src/components/Settings/Security/index.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ const PwdDate = ({ passwordCreated }: { passwordCreated: number }) => {
3636
);
3737
};
3838

39-
export const Security = () => {
39+
export const Security = ({
40+
showRecoveryKeyV2,
41+
}: {
42+
showRecoveryKeyV2?: boolean;
43+
}) => {
4044
const { passwordCreated, hasPassword } = useAccount();
4145
const { l10n } = useLocalization();
4246
const localizedNotSet = l10n.getString('security-not-set', null, 'Not Set');
@@ -81,7 +85,7 @@ export const Security = () => {
8185
</Localized>
8286
<hr className="unit-row-hr" />
8387

84-
<UnitRowRecoveryKey />
88+
<UnitRowRecoveryKey {...{ showRecoveryKeyV2 }} />
8589
<hr className="unit-row-hr" />
8690
<UnitRowTwoStepAuth />
8791

0 commit comments

Comments
 (0)