Skip to content

Commit 5c26974

Browse files
AshCorrcoldlink
andcommitted
feat: Add Google One Tap proof of concept
Co-authored-by: Mahesh Makani <[email protected]>
1 parent 1e910f6 commit 5c26974

File tree

3 files changed

+142
-1
lines changed

3 files changed

+142
-1
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { loadScript, log } from '@guardian/libs';
2+
import { useEffect } from 'react';
3+
import { useIsSignedIn } from '../lib/useAuthStatus';
4+
5+
const getGSIConfiguration = (): { clientId: string; loginUri: string } => {
6+
switch (window.guardian.config.stage) {
7+
case 'PROD':
8+
return {
9+
clientId: 'PROD CLIENT ID',
10+
loginUri: 'https://profile.thegulocal.com/signin/google',
11+
};
12+
case 'CODE':
13+
return {
14+
clientId: 'CODE CLIENT ID',
15+
loginUri: 'https://profile.thegulocal.com/signin/google',
16+
};
17+
default:
18+
return {
19+
clientId:
20+
'774465807556-pkevncqpfs9486ms0bo5q1f2g9vhpior.apps.googleusercontent.com',
21+
loginUri: 'https://profile.thegulocal.com/signin/google',
22+
};
23+
}
24+
};
25+
26+
const initializeGoogleOneTap = () => (response: { credential: string }) => {
27+
const { credential } = response;
28+
29+
const queryParams = new URLSearchParams();
30+
queryParams.append('got', credential);
31+
queryParams.append('returnUrl', window.location.href);
32+
33+
window.location.replace(
34+
`https://profile.thegulocal.com/signin/google?${queryParams.toString()}`,
35+
);
36+
};
37+
38+
type PromptMomentNotification = {
39+
isSkippedMoment: () => boolean;
40+
isDismissedMoment: () => boolean;
41+
getDismissedReason: () =>
42+
| 'credential_returned'
43+
| 'cancel_called'
44+
| 'flow_restarted';
45+
getMomentType: () => 'display' | 'skipped' | 'dismissed';
46+
};
47+
48+
export type GoogleIdentityService = {
49+
initialize: (config: {
50+
client_id: string;
51+
login_uri: string;
52+
callback: (response: { credential: string }) => void;
53+
auto_select?: boolean;
54+
cancel_on_tap_outside?: boolean;
55+
use_fedcm_for_prompt?: boolean;
56+
}) => void;
57+
prompt: (
58+
callback: (momentNotification: PromptMomentNotification) => void,
59+
) => void;
60+
};
61+
62+
const loadGSI = async (): Promise<GoogleIdentityService> => {
63+
log('identity', 'Loading Google Sign-in Services (GSI)');
64+
// TODO: Can we invoke the built-in FedCM API instead of using GSI?
65+
// This would reduce our dpenedency on a third-party library and all of the privacy
66+
// implications that come with it. It would also save loading ~80KB of JS.
67+
await loadScript('https://accounts.google.com/gsi/client').catch((e) => {
68+
throw new Error(
69+
`Failed to initialize Google One Tap: failed to load GSI`,
70+
{ cause: e },
71+
);
72+
});
73+
74+
if (!window.google?.accounts?.id) {
75+
throw new Error('Failed to initialize Google One Tap: GSI not found');
76+
}
77+
78+
log('identity', 'Loaded Google Sign-in Services (GSI)');
79+
return window.google.accounts.id;
80+
};
81+
82+
export const GoogleOneTap = () => {
83+
const isSignedIn = useIsSignedIn();
84+
85+
useEffect(() => {
86+
if (isSignedIn === true) {
87+
log(
88+
'identity',
89+
'User is already signed in, skipping Google One Tap initialization',
90+
);
91+
return;
92+
} else if (isSignedIn === 'Pending') {
93+
// If the auth status is still pending, we don't want to initialize Google One Tap yet.
94+
log(
95+
'identity',
96+
'User auth state is still pending, delaying Google One Tap initialization',
97+
);
98+
return;
99+
}
100+
101+
const { clientId, loginUri } = getGSIConfiguration();
102+
103+
void loadGSI().then((gsi) => {
104+
log('identity', 'Initializing Google One Tap', {
105+
clientId,
106+
loginUri,
107+
});
108+
109+
gsi.initialize({
110+
client_id: clientId,
111+
login_uri: loginUri,
112+
callback: initializeGoogleOneTap,
113+
auto_select: true,
114+
cancel_on_tap_outside: false,
115+
use_fedcm_for_prompt: true,
116+
});
117+
118+
log('identity', 'Requesting Google One Tap prompt');
119+
gsi.prompt((notifcation) => {
120+
// TODO: Handle tracking of the prompt moment notification. Ophan?
121+
log('identity', 'Google One Tap prompt notification received', {
122+
isSkippedMoment: notifcation.isSkippedMoment(),
123+
isDismissedMoment: notifcation.isDismissedMoment(),
124+
dismissedReason: notifcation.getDismissedReason(),
125+
momentType: notifcation.getMomentType(),
126+
});
127+
});
128+
});
129+
}, [isSignedIn]);
130+
131+
return <></>;
132+
};

dotcom-rendering/src/layouts/FrontLayout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
MobileAdSlot,
2222
} from '../components/FrontsAdSlots';
2323
import { FrontSection } from '../components/FrontSection';
24+
import { GoogleOneTap } from '../components/GoogleOneTap.importable';
2425
import { HeaderAdSlot } from '../components/HeaderAdSlot';
2526
import { Island } from '../components/Island';
2627
import { LabsHeader } from '../components/LabsHeader';
@@ -193,6 +194,9 @@ export const FrontLayout = ({ front, NAV }: Props) => {
193194

194195
return (
195196
<>
197+
<Island priority="enhancement" defer={{ until: 'idle' }}>
198+
<GoogleOneTap />
199+
</Island>
196200
<div data-print-layout="hide" id="bannerandheader">
197201
{renderAds && (
198202
<Stuck>

dotcom-rendering/window.guardian.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
} from '@guardian/libs';
88
import type ophan from '@guardian/ophan-tracker-js';
99
import type { WeeklyArticleHistory } from '@guardian/support-dotcom-components/dist/dotcom/types';
10+
import type { GoogleIdentityService } from './src/components/GoogleOneTap.importable';
1011
import type { google } from './src/components/YoutubeAtom/ima';
1112
import type { DailyArticleHistory } from './src/lib/dailyArticleCount';
1213
import type { ReaderRevenueDevUtils } from './src/lib/readerRevenueDevUtils';
@@ -67,7 +68,11 @@ declare global {
6768
) => boolean;
6869
};
6970
mockLiveUpdate: (data: LiveUpdateType) => void;
70-
google?: typeof google;
71+
google?: typeof google & {
72+
accounts?: {
73+
id?: GoogleIdentityService;
74+
};
75+
};
7176
YT?: typeof YT;
7277
onYouTubeIframeAPIReady?: () => void;
7378
}

0 commit comments

Comments
 (0)