Skip to content

Commit 1e50b8f

Browse files
committed
fix(auth): Fix issues with deeplinking third party auth
1 parent 93af729 commit 1e50b8f

19 files changed

Lines changed: 172 additions & 22 deletions

File tree

packages/fxa-settings/src/components/ThirdPartyAuth/index.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,36 @@ describe('ThirdPartyAuthComponent', () => {
136136
expect(onContinueWithApple).not.toBeCalled();
137137
});
138138

139+
it('should deeplink directly to google auth, if deeplink=`googleLogin`', async () => {
140+
renderWith({
141+
enabled: true,
142+
showSeparator: false,
143+
deeplink: 'googleLogin',
144+
view: 'index'
145+
});
146+
147+
expect(
148+
(await screen.findByTestId('google-signin-form-state')).getAttribute(
149+
'value'
150+
)
151+
).not.toEqual('');
152+
})
153+
154+
it('should deeplink directly to apple auth, if deeplink=`appleLogin`', async () => {
155+
renderWith({
156+
enabled: true,
157+
showSeparator: false,
158+
deeplink: 'appleLogin',
159+
view: 'index'
160+
});
161+
162+
expect(
163+
(await screen.findByTestId('apple-signin-form-state')).getAttribute(
164+
'value'
165+
)
166+
).not.toEqual('');
167+
})
168+
139169
it('hides separator', async () => {
140170
renderWith({
141171
enabled: true,

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

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import React, { useRef, FormEventHandler, useEffect } from 'react';
5+
import React, { useRef, FormEventHandler, useEffect, useCallback } from 'react';
66
import { FtlMsg } from 'fxa-react/lib/utils';
77

88
import { ReactComponent as GoogleLogo } from './google-logo.svg';
@@ -18,6 +18,7 @@ export type ThirdPartyAuthProps = {
1818
onContinueWithApple?: FormEventHandler<HTMLFormElement>;
1919
showSeparator?: boolean;
2020
viewName?: string;
21+
deeplink?: string;
2122
};
2223

2324
/**
@@ -30,6 +31,7 @@ const ThirdPartyAuth = ({
3031
onContinueWithApple,
3132
showSeparator = true,
3233
viewName = 'unknown',
34+
deeplink,
3335
}: ThirdPartyAuthProps) => {
3436
const config = useConfig();
3537

@@ -76,6 +78,7 @@ const ThirdPartyAuth = ({
7678
</FtlMsg>
7779
</>
7880
),
81+
deeplink
7982
}}
8083
/>
8184
<ThirdPartySignInForm
@@ -98,6 +101,7 @@ const ThirdPartyAuth = ({
98101
</FtlMsg>
99102
</>
100103
),
104+
deeplink
101105
}}
102106
/>
103107
</div>
@@ -125,6 +129,7 @@ const ThirdPartySignInForm = ({
125129
buttonText,
126130
onSubmit,
127131
viewName,
132+
deeplink
128133
}: {
129134
party: 'google' | 'apple';
130135
authorizationEndpoint: string;
@@ -139,11 +144,14 @@ const ThirdPartySignInForm = ({
139144
buttonText: ReactElement;
140145
onSubmit?: FormEventHandler<HTMLFormElement>;
141146
viewName?: string;
147+
deeplink?: string;
142148
}) => {
143149
const { logViewEventOnce } = useMetrics();
144150
const stateRef = useRef<HTMLInputElement>(null);
151+
const formRef = useRef<HTMLFormElement>(null);
152+
const isDeeplinking = deeplink !== undefined;
145153

146-
async function onClick() {
154+
const onClick = useCallback(async () => {
147155
logViewEventOnce(`flow.${party}`, 'oauth-start');
148156

149157
switch (`${party}-${viewName}`) {
@@ -165,20 +173,40 @@ const ThirdPartySignInForm = ({
165173
case 'apple-signup':
166174
GleanMetrics.thirdPartyAuth.startAppleAuthFromReg();
167175
break;
176+
case 'google-deeplink':
177+
GleanMetrics.thirdPartyAuth.googleDeeplink();
178+
break;
179+
case 'apple-deeplink':
180+
GleanMetrics.thirdPartyAuth.appleDeeplink();
181+
break;
168182
}
169183

170184
// wait for all the Glean events to be sent before the page unloads
171185
await GleanMetrics.isDone();
172186

173-
stateRef.current!.value = getState();
174-
}
187+
if (stateRef.current) {
188+
stateRef.current.value = getState();
189+
}
190+
}, [party, viewName, logViewEventOnce]);
191+
175192

176193
if (onSubmit === undefined) {
177194
onSubmit = () => true;
178195
}
179196

197+
useEffect(() => {
198+
if (deeplink && formRef.current) {
199+
// Only deeplink if this is the correct button
200+
if ((deeplink === "googleLogin" && party === "google") || (deeplink === "appleLogin" && party === "apple")) {
201+
onClick();
202+
formRef.current.submit();
203+
}
204+
}
205+
206+
}, [deeplink, onClick, party]);
207+
180208
return (
181-
<form action={authorizationEndpoint} method="GET" onSubmit={onSubmit}>
209+
<form action={authorizationEndpoint} method="GET" onSubmit={onSubmit} ref={formRef}>
182210
<input
183211
data-testid={`${party}-signin-form-state`}
184212
ref={stateRef}
@@ -196,13 +224,15 @@ const ThirdPartySignInForm = ({
196224
<input type="hidden" name="response_mode" value={responseMode} />
197225
)}
198226

199-
<button
200-
type="submit"
201-
className="w-full px-2 mt-2 justify-center text-black bg-transparent border-grey-300 border hover:border-black rounded-lg text-md text-center inline-flex items-center focus-visible-default outline-offset-2"
202-
onClick={onClick}
203-
>
204-
{buttonText}
205-
</button>
227+
{!isDeeplinking ? (
228+
<button
229+
type="submit"
230+
className="w-full px-2 mt-2 justify-center text-black bg-transparent border-grey-300 border hover:border-black rounded-lg text-md text-center inline-flex items-center focus-visible-default outline-offset-2"
231+
onClick={onClick}
232+
>
233+
{buttonText}
234+
</button>
235+
) : null }
206236
</form>
207237
);
208238
};
@@ -213,7 +243,7 @@ function deleteParams(searchParams: URLSearchParams, paramsToDelete: string[]) {
213243
}
214244

215245
function getState() {
216-
// We stash originating location in the state oauth param
246+
// We stash the originating location in the state oauth param
217247
// because we will need it to use it to reconstruct the redirect URL for RP
218248
const params = new URLSearchParams(window.location.search);
219249
// we won't need these params that are used for internal backbone/react navigation
@@ -225,12 +255,11 @@ function getState() {
225255
'forceExperimentGroup',
226256
'showReactApp',
227257
]);
228-
const state = encodeURIComponent(
258+
return encodeURIComponent(
229259
`${window.location.origin}${
230260
window.location.pathname
231261
}?${modifiedParams.toString()}`
232262
);
233-
return state;
234263
}
235264

236265
export default ThirdPartyAuth;

packages/fxa-settings/src/lib/glean/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,12 @@ const recordEventMetric = (
415415
case 'third_party_auth_apple_login_start':
416416
thirdPartyAuth.appleLoginStart.record();
417417
break;
418+
case 'third_party_auth_apple_deeplink':
419+
thirdPartyAuth.appleDeeplink.record();
420+
break;
421+
case 'third_party_auth_google_deeplink':
422+
thirdPartyAuth.googleDeeplink.record();
423+
break;
418424
case 'cad_firefox_notnow_submit':
419425
cadFirefox.notnowSubmit.record();
420426
break;

packages/fxa-settings/src/models/pages/index/query-params.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { IsEmail, IsOptional } from 'class-validator';
5+
import { IsEmail, IsIn, IsOptional } from 'class-validator';
66
import {
77
bind,
88
KeyTransforms,
@@ -19,4 +19,9 @@ export class IndexQueryParams extends ModelDataProvider {
1919
@IsEmail()
2020
@bind(KeyTransforms.snakeCase)
2121
loginHint: string | undefined;
22+
23+
@IsOptional()
24+
@IsIn(['googleLogin', 'appleLogin'])
25+
@bind()
26+
deeplink: string | undefined;
2227
}

packages/fxa-settings/src/models/pages/signin/query-params.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
MaxLength,
1212
Matches,
1313
Validate,
14+
IsIn,
1415
} from 'class-validator';
1516
import {
1617
bind,
@@ -50,4 +51,9 @@ export class SigninQueryParams extends ModelDataProvider {
5051
@Validate(IsFxaRedirectToUrl, {})
5152
@bind(T.snakeCase)
5253
redirectTo: string | undefined = undefined;
54+
55+
@IsOptional()
56+
@IsIn(['googleLogin', 'appleLogin'])
57+
@bind(T.snakeCase)
58+
deeplink!: string;
5359
}

packages/fxa-settings/src/models/pages/signup/query-params.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { IsBoolean, IsEmail, IsOptional } from 'class-validator';
6-
import { bind, ModelDataProvider } from '../../../lib/model-data';
5+
import { IsBoolean, IsEmail, IsIn, IsOptional } from 'class-validator';
6+
import { bind, KeyTransforms as T, ModelDataProvider } from '../../../lib/model-data';
77

88
export class SignupQueryParams extends ModelDataProvider {
99
// 'email' will be optional once the index page is converted to React
@@ -20,4 +20,9 @@ export class SignupQueryParams extends ModelDataProvider {
2020
@IsBoolean()
2121
@bind()
2222
emailStatusChecked: boolean = false;
23+
24+
@IsOptional()
25+
@IsIn(['googleLogin', 'appleLogin'])
26+
@bind(T.snakeCase)
27+
deeplink!: string;
2328
}

packages/fxa-settings/src/pages/Index/container.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as Sentry from '@sentry/browser';
66

7-
import { useCallback, useEffect, useState } from 'react';
7+
import React, { useCallback, useEffect, useState } from 'react';
88
import { RouteComponentProps, useLocation } from '@reach/router';
99
import { isEmail } from 'class-validator';
1010

@@ -252,6 +252,7 @@ export const IndexContainer = ({
252252
}, [ftlMsgResolver, deleteAccountSuccess]);
253253

254254
const initialPrefill = prefillEmail || suggestedEmail;
255+
const deeplink = queryParamModel.deeplink;
255256

256257
return isLoading ? (
257258
<LoadingSpinner fullScreen />
@@ -267,6 +268,7 @@ export const IndexContainer = ({
267268
errorBannerMessage,
268269
successBannerMessage,
269270
tooltipErrorMessage,
271+
deeplink
270272
}}
271273
prefillEmail={initialPrefill}
272274
/>

packages/fxa-settings/src/pages/Index/index.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ describe('Index page', () => {
115115
);
116116
});
117117

118+
it('does not render when deeplinking third party auth', () => {
119+
renderWithLocalizationProvider(
120+
<Subject
121+
integration={createMockIndexOAuthIntegration({
122+
clientId: POCKET_CLIENTIDS[0],
123+
})}
124+
deeplink="appleLogin"
125+
/>
126+
);
127+
128+
thirdPartyAuthNotRendered();
129+
});
130+
118131
it('renders as expected when client is Pocket', () => {
119132
renderWithLocalizationProvider(
120133
<Subject

packages/fxa-settings/src/pages/Index/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const Index = ({
3131
setErrorBannerMessage,
3232
setSuccessBannerMessage,
3333
setTooltipErrorMessage,
34+
deeplink
3435
}: IndexProps) => {
3536
const clientId = integration.getClientId();
3637
const isSync = integration.isSync();
@@ -42,10 +43,9 @@ export const Index = ({
4243

4344
const emailEngageEventEmitted = useRef(false);
4445

46+
const isDeeplinking = !!deeplink;
47+
4548
useEffect(() => {
46-
// Note we might not need this later due to automatic page load events,
47-
// but it's here for now to match parity with Backbone. This will be closely
48-
// monitored for the `service=relay` flow for some time.
4949
GleanMetrics.emailFirst.view();
5050
}, []);
5151

@@ -76,6 +76,11 @@ export const Index = ({
7676
},
7777
});
7878

79+
if (isDeeplinking) {
80+
// To avoid flickering, we just render third party auth when deeplinking
81+
return <ThirdPartyAuth showSeparator={false} viewName="deeplink" deeplink={deeplink} />
82+
}
83+
7984
return (
8085
<AppLayout>
8186
{isSync ? (

packages/fxa-settings/src/pages/Index/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface IndexProps extends LocationState {
3131
errorBannerMessage?: string;
3232
successBannerMessage?: string;
3333
tooltipErrorMessage?: string;
34+
deeplink?: string
3435
}
3536

3637
export interface IndexFormData {

0 commit comments

Comments
 (0)