Skip to content

Commit 125e37d

Browse files
authored
Merge pull request #20333 from mozilla/FXA-13397
feat(ui): For new send tab entrypoints, always navigate directly to /pair
2 parents 5ab20fd + fbae03c commit 125e37d

8 files changed

Lines changed: 277 additions & 7 deletions

File tree

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,35 @@ test.describe('severity-1 #smoke', () => {
104104
await expect(page).toHaveURL(/pair/);
105105
await signup.checkWebChannelMessage(FirefoxCommand.OAuthLogin);
106106
});
107+
108+
test('signup oauth webchannel with Sync desktop and send-tab entrypoint skips signup_confirmed_sync', async ({
109+
target,
110+
syncOAuthBrowserPages: { confirmSignupCode, page, signup },
111+
testAccountTracker,
112+
}) => {
113+
const { email, password } =
114+
testAccountTracker.generateSignupAccountDetails();
115+
116+
const sendTabParams = new URLSearchParams(syncDesktopOAuthQueryParams);
117+
sendTabParams.set('entrypoint', 'send-tab-toolbar-icon');
118+
119+
await signup.goto('/', sendTabParams);
120+
121+
await signup.fillOutEmailForm(email);
122+
123+
await expect(signup.signupFormHeading).toBeVisible();
124+
125+
await signup.fillOutSyncSignupForm(password);
126+
127+
await expect(page).toHaveURL(/confirm_signup_code/);
128+
129+
const code = await target.emailClient.getVerifyShortCode(email);
130+
await confirmSignupCode.fillOutCodeForm(code);
131+
132+
await expect(page).toHaveURL(/pair/);
133+
await expect(page).toHaveURL(/signupSuccess=true/);
134+
await expect(page).toHaveURL(/showSuccessMessage=true/);
135+
await signup.checkWebChannelMessage(FirefoxCommand.OAuthLogin);
136+
});
107137
});
108138
});

packages/fxa-content-server/app/scripts/templates/pair/index.mustache

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@
44
{{#showSuccessMessage}}
55
<div id="fxa-connected-heading"
66
class="w-full text-md font-bold p-3 mb-5 rounded bg-green-500 text-grey-900 dark:text-grey-900 flex items-center justify-center before:content-icon-circle-check-outline before:flex">
7-
<span class="ps-3 align-middle">{{#t}}Signed in successfully!{{/t}}</span>
7+
{{#showPasswordCreatedMessage}}
8+
<span class="ps-3 align-middle">{{#t}}Password created. You’re now syncing.{{/t}}</span>
9+
{{/showPasswordCreatedMessage}}
10+
{{^showPasswordCreatedMessage}}
11+
{{#showSignupSuccessMessage}}
12+
<span class="ps-3 align-middle">{{#t}}Account created. You’re now syncing.{{/t}}</span>
13+
{{/showSignupSuccessMessage}}
14+
{{^showSignupSuccessMessage}}
15+
<span class="ps-3 align-middle">{{#t}}Signed in successfully!{{/t}}</span>
16+
{{/showSignupSuccessMessage}}
17+
{{/showPasswordCreatedMessage}}
818
</div>
919
{{/showSuccessMessage}}
1020

packages/fxa-content-server/app/scripts/views/pair/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ class PairIndexView extends FormView {
191191
graphicId,
192192
needsMobileConfirmed,
193193
showSuccessMessage: this.showSuccessMessage(),
194+
showSignupSuccessMessage: this.showSignupSuccessMessage(),
195+
showPasswordCreatedMessage: this.showPasswordCreatedMessage(),
194196
buttonTextShadowClass,
195197
tabletBackArrowColor,
196198
});
@@ -275,6 +277,14 @@ class PairIndexView extends FormView {
275277
!!this.getSearchParam('showSuccessMessage'))
276278
);
277279
}
280+
281+
showSignupSuccessMessage() {
282+
return !!this.getSearchParam('signupSuccess');
283+
}
284+
285+
showPasswordCreatedMessage() {
286+
return !!this.getSearchParam('passwordCreated');
287+
}
278288
}
279289

280290
Cocktail.mixin(

packages/fxa-content-server/app/tests/spec/views/pair/index.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,4 +424,37 @@ describe('views/pair/index', () => {
424424
});
425425
});
426426
});
427+
428+
describe('success message variants', () => {
429+
it('showSuccessMessage returns true when showSuccessMessage query param is present', () => {
430+
windowMock.location.search = '?showSuccessMessage=true';
431+
assert.isTrue(view.showSuccessMessage());
432+
});
433+
434+
it('showSuccessMessage returns false when needsMobileConfirmed is set', () => {
435+
windowMock.location.search = '?showSuccessMessage=true';
436+
view.model.set('needsMobileConfirmed', true);
437+
assert.isFalse(view.showSuccessMessage());
438+
});
439+
440+
it('showSignupSuccessMessage returns true when signupSuccess query param is present', () => {
441+
windowMock.location.search = '?signupSuccess=true';
442+
assert.isTrue(view.showSignupSuccessMessage());
443+
});
444+
445+
it('showSignupSuccessMessage returns false when signupSuccess query param is absent', () => {
446+
windowMock.location.search = '?showSuccessMessage=true';
447+
assert.isFalse(view.showSignupSuccessMessage());
448+
});
449+
450+
it('showPasswordCreatedMessage returns true when passwordCreated query param is present', () => {
451+
windowMock.location.search = '?passwordCreated=true';
452+
assert.isTrue(view.showPasswordCreatedMessage());
453+
});
454+
455+
it('showPasswordCreatedMessage returns false when passwordCreated query param is absent', () => {
456+
windowMock.location.search = '?showSuccessMessage=true';
457+
assert.isFalse(view.showPasswordCreatedMessage());
458+
});
459+
});
427460
});

packages/fxa-settings/src/constants/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,25 @@ export enum ENTRYPOINTS {
2020
FIREFOX_SYNCED_TABS_ENTRYPOINT = 'synced-tabs',
2121
FIREFOX_TABS_SIDEBAR_ENTRYPOINT = 'tabs-sidebar',
2222
FIREFOX_FX_VIEW_ENTRYPOINT = 'fx-view',
23+
SEND_TAB_TAB_CONTEXT_MENU = 'send-tab-tab-context-menu',
24+
SEND_TAB_ACCOUNT_MENU = 'send-tab-account-menu',
25+
SEND_TAB_APP_MENU = 'send-tab-app-menu',
26+
SEND_TAB_FIREFOX_VIEW_THREE_DOTS = 'send-tab-firefox-view-three-dots',
27+
SEND_TAB_LINK_CONTEXT_MENU = 'send-tab-link-context-menu',
28+
SEND_TAB_PAGE_CONTEXT_MENU = 'send-tab-page-context-menu',
29+
SEND_TAB_TOOLBAR_ICON = 'send-tab-toolbar-icon',
2330
}
2431

32+
export const SEND_TAB_ENTRYPOINTS = new Set<string>([
33+
ENTRYPOINTS.SEND_TAB_TAB_CONTEXT_MENU,
34+
ENTRYPOINTS.SEND_TAB_ACCOUNT_MENU,
35+
ENTRYPOINTS.SEND_TAB_APP_MENU,
36+
ENTRYPOINTS.SEND_TAB_FIREFOX_VIEW_THREE_DOTS,
37+
ENTRYPOINTS.SEND_TAB_LINK_CONTEXT_MENU,
38+
ENTRYPOINTS.SEND_TAB_PAGE_CONTEXT_MENU,
39+
ENTRYPOINTS.SEND_TAB_TOOLBAR_ICON,
40+
]);
41+
2542
export const LINK = {
2643
AMO: 'https://addons.mozilla.org/',
2744
FX_DESKTOP: 'https://www.mozilla.org/firefox/new/',

packages/fxa-settings/src/pages/Signin/utils.test.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import {
1717
createMockSigninOAuthNativeIntegration,
1818
createMockSigninOAuthIntegration,
1919
} from './mocks';
20-
import { handleNavigation, ensureCanLinkAcountOrRedirect } from './utils';
20+
import {
21+
handleNavigation,
22+
ensureCanLinkAcountOrRedirect,
23+
getSyncNavigate,
24+
isSendTabEntrypoint,
25+
} from './utils';
26+
import { SEND_TAB_ENTRYPOINTS } from '../../constants';
2127
import * as ReachRouter from '@reach/router';
2228
import * as ReactUtils from 'fxa-react/lib/utils';
2329
import firefox from '../../lib/channels/firefox';
@@ -344,6 +350,65 @@ describe('Signin utils', () => {
344350
);
345351
});
346352
});
353+
354+
describe('handleNavigation with send-tab entrypoints', () => {
355+
const createSendTabNavigationOptions = (
356+
overrides: Partial<NavigationOptions> = {}
357+
): NavigationOptions =>
358+
({
359+
email: MOCK_EMAIL,
360+
signinData: {
361+
uid: MOCK_UID,
362+
sessionToken: MOCK_SESSION_TOKEN,
363+
emailVerified: true,
364+
sessionVerified: true,
365+
verificationMethod: VerificationMethods.EMAIL,
366+
verificationReason: VerificationReasons.SIGN_IN,
367+
keyFetchToken: MOCK_KEY_FETCH_TOKEN,
368+
},
369+
redirectTo: '',
370+
finishOAuthFlowHandler: jest
371+
.fn()
372+
.mockReturnValue(MOCK_OAUTH_FLOW_HANDLER_RESPONSE),
373+
queryParams: '',
374+
...overrides,
375+
}) as NavigationOptions;
376+
377+
it('clears showInlineRecoveryKeySetup for send-tab sign-in and navigates to /pair', async () => {
378+
const navigationOptions = createSendTabNavigationOptions({
379+
integration: createMockSigninOAuthNativeSyncIntegration(),
380+
queryParams: '?service=sync&entrypoint=send-tab-toolbar-icon',
381+
showInlineRecoveryKeySetup: true,
382+
handleFxaLogin: true,
383+
});
384+
385+
await handleNavigation(navigationOptions);
386+
387+
expect(hardNavigateSpy).toHaveBeenCalled();
388+
const navigatedUrl = hardNavigateSpy.mock.calls[0][0] as string;
389+
expect(navigatedUrl).toContain('/pair?');
390+
expect(navigatedUrl).toContain('showSuccessMessage=true');
391+
expect(navigatedUrl).not.toContain('inline_recovery_key');
392+
});
393+
394+
it('clears showSignupConfirmedSync for send-tab post-verify and navigates to /pair with passwordCreated', async () => {
395+
const navigationOptions = createSendTabNavigationOptions({
396+
integration: createMockSigninOAuthNativeSyncIntegration(),
397+
queryParams: '?service=sync&entrypoint=send-tab-app-menu',
398+
showSignupConfirmedSync: true,
399+
origin: 'post-verify-set-password',
400+
handleFxaLogin: true,
401+
});
402+
403+
await handleNavigation(navigationOptions);
404+
405+
expect(hardNavigateSpy).toHaveBeenCalled();
406+
const navigatedUrl = hardNavigateSpy.mock.calls[0][0] as string;
407+
expect(navigatedUrl).toContain('/pair?');
408+
expect(navigatedUrl).toContain('showSuccessMessage=true');
409+
expect(navigatedUrl).toContain('passwordCreated=true');
410+
});
411+
});
347412
});
348413

349414
describe('ensureCanLinkAcountOrRedirect', () => {
@@ -400,4 +465,69 @@ describe('Signin utils', () => {
400465
});
401466
});
402467
});
468+
469+
describe('isSendTabEntrypoint', () => {
470+
it('returns true for all send-tab entrypoints', () => {
471+
for (const entrypoint of SEND_TAB_ENTRYPOINTS) {
472+
expect(isSendTabEntrypoint(`?entrypoint=${entrypoint}`)).toBe(true);
473+
}
474+
});
475+
476+
it('returns false for non-send-tab entrypoints', () => {
477+
expect(isSendTabEntrypoint('?entrypoint=preferences')).toBe(false);
478+
expect(isSendTabEntrypoint('?entrypoint=fxa_app_menu')).toBe(false);
479+
expect(isSendTabEntrypoint('')).toBe(false);
480+
expect(isSendTabEntrypoint('?service=sync')).toBe(false);
481+
});
482+
});
483+
484+
describe('getSyncNavigate', () => {
485+
it('returns /inline_recovery_key_setup when showInlineRecoveryKeySetup is true', () => {
486+
const result = getSyncNavigate('?service=sync', {
487+
showInlineRecoveryKeySetup: true,
488+
});
489+
expect(result.to).toContain('/inline_recovery_key_setup?');
490+
});
491+
492+
it('returns /signup_confirmed_sync when showSignupConfirmedSync is true', () => {
493+
const result = getSyncNavigate('?service=sync', {
494+
showSignupConfirmedSync: true,
495+
});
496+
expect(result.to).toContain('/signup_confirmed_sync?');
497+
});
498+
499+
describe('/pair redirect', () => {
500+
it('returns /pair with showSuccessMessage by default', () => {
501+
const result = getSyncNavigate('?service=sync');
502+
expect(result.to).toContain('/pair?');
503+
expect(result.to).toContain('showSuccessMessage=true');
504+
expect(result.shouldHardNavigate).toBe(true);
505+
});
506+
507+
it('includes signupSuccess param when signupSuccess is true', () => {
508+
const result = getSyncNavigate('?service=sync', {
509+
signupSuccess: true,
510+
});
511+
expect(result.to).toContain('/pair?');
512+
expect(result.to).toContain('signupSuccess=true');
513+
expect(result.to).toContain('showSuccessMessage=true');
514+
});
515+
516+
it('includes passwordCreated param when origin is post-verify-set-password', () => {
517+
const result = getSyncNavigate('?service=sync', {
518+
origin: 'post-verify-set-password',
519+
});
520+
expect(result.to).toContain('/pair?');
521+
expect(result.to).toContain('passwordCreated=true');
522+
expect(result.to).toContain('showSuccessMessage=true');
523+
});
524+
525+
it('does not include passwordCreated for other origins', () => {
526+
const result = getSyncNavigate('?service=sync', {
527+
origin: 'signup',
528+
});
529+
expect(result.to).not.toContain('passwordCreated');
530+
});
531+
});
532+
});
403533
});

packages/fxa-settings/src/pages/Signin/utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
useAuthClient,
1414
isOAuthWebIntegration,
1515
} from '../../models';
16+
import { SEND_TAB_ENTRYPOINTS } from '../../constants';
1617
import { FtlMsgResolver } from 'fxa-react/lib/utils';
1718
import { useNavigateWithQuery } from '../../lib/hooks/useNavigateWithQuery';
1819
import { navigate } from '@reach/router';
@@ -45,6 +46,8 @@ interface SyncNavigateOptions {
4546
isSignInWithThirdPartyAuth?: boolean;
4647
showSignupConfirmedSync?: boolean;
4748
syncHidePromoAfterLogin?: boolean;
49+
signupSuccess?: boolean;
50+
origin?: string;
4851
}
4952

5053
export function getSyncNavigate(
@@ -54,6 +57,8 @@ export function getSyncNavigate(
5457
isSignInWithThirdPartyAuth,
5558
showSignupConfirmedSync,
5659
syncHidePromoAfterLogin,
60+
signupSuccess,
61+
origin,
5762
}: SyncNavigateOptions = {}
5863
) {
5964
const searchParams = new URLSearchParams(queryParams);
@@ -87,6 +92,12 @@ export function getSyncNavigate(
8792
}
8893

8994
searchParams.set('showSuccessMessage', 'true');
95+
if (signupSuccess) {
96+
searchParams.set('signupSuccess', 'true');
97+
}
98+
if (origin === 'post-verify-set-password') {
99+
searchParams.set('passwordCreated', 'true');
100+
}
90101
return {
91102
to: `/pair?${searchParams}`,
92103
// TODO: don't hard navigate once Pair is converted to React
@@ -207,6 +218,16 @@ export async function handleNavigation(navigationOptions: NavigationOptions) {
207218
navigationOptions.syncHidePromoAfterLogin = true;
208219
}
209220

221+
// Send Tab entrypoints skip intermediate pages (inline recovery key, signup
222+
// confirmed sync) and go directly to the /pair choice screen.
223+
if (
224+
isSendTabEntrypoint(navigationOptions.queryParams) &&
225+
integration.isSync()
226+
) {
227+
navigationOptions.showInlineRecoveryKeySetup = false;
228+
navigationOptions.showSignupConfirmedSync = false;
229+
}
230+
210231
// When a session is unverified, we need to redirect to the appropriate page depending on status of
211232
// the account and the integration being used.
212233
// There are 3 types of unverified sessions:
@@ -467,13 +488,15 @@ const getNonOAuthNavigationTarget = async (
467488
redirectTo,
468489
isSignInWithThirdPartyAuth,
469490
showSignupConfirmedSync,
491+
origin,
470492
} = navigationOptions;
471493
if (integration.isSync()) {
472494
return {
473495
...getSyncNavigate(queryParams, {
474496
showInlineRecoveryKeySetup,
475497
isSignInWithThirdPartyAuth,
476498
showSignupConfirmedSync,
499+
origin,
477500
}),
478501
};
479502
}
@@ -501,6 +524,7 @@ const getOAuthNavigationTarget = async (
501524
navigationOptions.isSignInWithThirdPartyAuth,
502525
showSignupConfirmedSync: navigationOptions.showSignupConfirmedSync,
503526
syncHidePromoAfterLogin: navigationOptions.syncHidePromoAfterLogin,
527+
origin: navigationOptions.origin,
504528
}),
505529
locationState,
506530
};
@@ -556,6 +580,7 @@ const getOAuthNavigationTarget = async (
556580
navigationOptions.isSignInWithThirdPartyAuth,
557581
showSignupConfirmedSync: navigationOptions.showSignupConfirmedSync,
558582
syncHidePromoAfterLogin: navigationOptions.syncHidePromoAfterLogin,
583+
origin: navigationOptions.origin,
559584
}),
560585
oauthData: {
561586
code,
@@ -644,3 +669,8 @@ export async function ensureCanLinkAcountOrRedirect({
644669
}
645670
return ok;
646671
}
672+
673+
export function isSendTabEntrypoint(queryParams: string): boolean {
674+
const entrypoint = new URLSearchParams(queryParams).get('entrypoint');
675+
return entrypoint !== null && SEND_TAB_ENTRYPOINTS.has(entrypoint);
676+
}

0 commit comments

Comments
 (0)