Skip to content

Commit ecd1bec

Browse files
authored
fix(google-sign-in): initialize/renderButton/prompt helpers (#734)
1 parent 8a7c80e commit ecd1bec

9 files changed

Lines changed: 604 additions & 159 deletions

File tree

docs/content/scripts/google-sign-in.md

Lines changed: 129 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,95 @@ Nuxt Scripts provides a registry script composable [`useScriptGoogleSignIn()`{la
2727
::google-sign-in-demo
2828
::
2929

30+
## Composable API
31+
32+
`useScriptGoogleSignIn()`{lang="ts"} returns the standard script context (`status`, `proxy`, `onLoaded`, …) plus three helpers that wrap the most common flows. Schema options passed to the composable are merged into every call so you don't have to repeat `clientId`, `loginUri`, `uxMode` etc.
33+
34+
```ts
35+
const { initialize, renderButton, prompt, status, onLoaded, proxy } = useScriptGoogleSignIn({
36+
clientId: 'YOUR_CLIENT_ID',
37+
context: 'signin',
38+
useFedcmForPrompt: true,
39+
})
40+
```
41+
42+
### `initialize(config?)`{lang="ts"}
43+
44+
Calls `google.accounts.id.initialize()`{lang="ts"} with schema options merged with `config`. Internally guarded so multiple calls (e.g. across navigations and component remounts) are safe; Google's API logs an error if `initialize()`{lang="ts"} runs again after a button has been rendered, so the helper only forwards the first call.
45+
46+
```ts
47+
initialize({
48+
callback: (response) => {
49+
// verify response.credential server-side
50+
}
51+
})
52+
```
53+
54+
### `renderButton(parent, config?)`{lang="ts"}
55+
56+
Renders the personalized button. Auto-initializes if needed and is safe to re-render on locale change or navigation.
57+
58+
```vue
59+
<script setup lang="ts">
60+
const { initialize, renderButton } = useScriptGoogleSignIn()
61+
const buttonRef = useTemplateRef<HTMLDivElement>('buttonRef')
62+
63+
initialize({ callback: handleCredential })
64+
65+
watch(buttonRef, (el) => {
66+
if (el)
67+
renderButton(el, { text: 'continue_with', use_fedcm: true })
68+
}, { immediate: true })
69+
</script>
70+
71+
<template>
72+
<div ref="buttonRef" />
73+
</template>
74+
```
75+
76+
### `prompt(listener?)`{lang="ts"}
77+
78+
Shows the One Tap prompt. Auto-initializes if needed.
79+
80+
```ts
81+
prompt((notification) => {
82+
if (notification.isNotDisplayed())
83+
console.log('Not shown:', notification.getNotDisplayedReason())
84+
})
85+
```
86+
87+
### Switching locales
88+
89+
The button locale is a `renderButton` option, not an `initialize` one. To change the language, clear the container and re-render:
90+
91+
```ts
92+
watch([locale, buttonRef], ([newLocale, el]) => {
93+
if (!el)
94+
return
95+
el.innerHTML = ''
96+
renderButton(el, { locale: newLocale, use_fedcm: true })
97+
}, { immediate: true })
98+
```
99+
100+
::note
101+
Google may still fall back to the user's Google account language regardless of this option. This is a Google-side behavior and not configurable.
102+
::
103+
104+
### Redirect UX mode
105+
106+
With `uxMode: 'redirect'`, Google **POSTs** the credential to your `loginUri` server endpoint as `application/x-www-form-urlencoded` (fields: `credential`, `g_csrf_token`, `select_by`, …). The credential does **not** appear as a URL fragment after the redirect; it travels in the POST body, which your server then handles before redirecting the browser.
107+
108+
If you need the credential client-side (e.g. SPA with a separate API), use `uxMode: 'popup'` with a `callback` instead.
109+
110+
```ts
111+
const { initialize, renderButton } = useScriptGoogleSignIn({
112+
uxMode: 'redirect',
113+
loginUri: 'https://your-server.com/auth/google',
114+
})
115+
116+
initialize() // no callback needed in redirect mode
117+
```
118+
30119
## Moment Notifications
31120

32121
Track the One Tap display state:
@@ -86,6 +175,24 @@ export default defineEventHandler(async (event) => {
86175
})
87176
```
88177

178+
## Cross-Origin-Opener-Policy
179+
180+
In `popup` UX mode (the default), Google opens a popup that delivers the credential back to your page via `window.postMessage`. If your page sends a strict `Cross-Origin-Opener-Policy` header, the browser will block that message and the sign-in will appear to do nothing: the popup closes but no callback fires.
181+
182+
If you set COOP at all, use `same-origin-allow-popups`:
183+
184+
```ts [nuxt.config.ts]
185+
export default defineNuxtConfig({
186+
routeRules: {
187+
'/login/**': {
188+
headers: { 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups' },
189+
},
190+
},
191+
})
192+
```
193+
194+
Pages without an explicit COOP header work by default. Redirect mode (`uxMode: 'redirect'`) and FedCM-only flows are unaffected.
195+
89196
## FedCM API Support
90197

91198
Enable Privacy Sandbox [FedCM API](https://developers.google.com/identity/gsi/web/guides/fedcm-migration) support for enhanced privacy. **FedCM adoption becomes mandatory in August 2025.**
@@ -204,30 +311,20 @@ The One Tap prompt provides a simplified sign-in experience:
204311

205312
```vue
206313
<script setup lang="ts">
207-
const { onLoaded } = useScriptGoogleSignIn()
314+
const { initialize, prompt } = useScriptGoogleSignIn({
315+
context: 'signin',
316+
useFedcmForPrompt: true,
317+
})
208318
209319
async function handleCredentialResponse(response: CredentialResponse) {
210-
// Send the credential to your backend for verification
211320
await $fetch('/api/auth/google', {
212321
method: 'POST',
213322
body: { credential: response.credential }
214323
})
215324
}
216325
217-
onMounted(() => {
218-
onLoaded(({ accounts }) => {
219-
accounts.id.initialize({
220-
client_id: 'YOUR_CLIENT_ID',
221-
callback: handleCredentialResponse,
222-
context: 'signin',
223-
ux_mode: 'popup',
224-
use_fedcm_for_prompt: true // Use Privacy Sandbox FedCM API
225-
})
226-
227-
// Show One Tap
228-
accounts.id.prompt()
229-
})
230-
})
326+
initialize({ callback: handleCredentialResponse })
327+
onMounted(() => prompt())
231328
</script>
232329
```
233330

@@ -237,35 +334,30 @@ Render Google's personalized Sign in with Google button:
237334

238335
```vue
239336
<script setup lang="ts">
240-
const { onLoaded } = useScriptGoogleSignIn()
337+
const { initialize, renderButton } = useScriptGoogleSignIn()
338+
const buttonRef = useTemplateRef<HTMLDivElement>('buttonRef')
241339
242340
function handleCredentialResponse(response: CredentialResponse) {
243341
console.log('Signed in!', response.credential)
244342
}
245343
246-
onMounted(() => {
247-
onLoaded(({ accounts }) => {
248-
accounts.id.initialize({
249-
client_id: 'YOUR_CLIENT_ID',
250-
callback: handleCredentialResponse
344+
initialize({ callback: handleCredentialResponse })
345+
346+
watch(buttonRef, (el) => {
347+
if (el) {
348+
renderButton(el, {
349+
type: 'standard',
350+
theme: 'outline',
351+
size: 'large',
352+
text: 'signin_with',
353+
shape: 'rectangular',
354+
logo_alignment: 'left',
251355
})
252-
253-
const buttonDiv = document.getElementById('g-signin-button')
254-
if (buttonDiv) {
255-
accounts.id.renderButton(buttonDiv, {
256-
type: 'standard',
257-
theme: 'outline',
258-
size: 'large',
259-
text: 'signin_with',
260-
shape: 'rectangular',
261-
logo_alignment: 'left'
262-
})
263-
}
264-
})
265-
})
356+
}
357+
}, { immediate: true })
266358
</script>
267359
268360
<template>
269-
<div id="g-signin-button" />
361+
<div ref="buttonRef" />
270362
</template>
271363
```

packages/script/src/registry-types.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,11 @@
452452
"name": "GoogleSignInOptions",
453453
"kind": "const",
454454
"code": "export const GoogleSignInOptions = object({\n /**\n * Your Google API client ID.\n * @example 'XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com'\n * @see https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid\n */\n clientId: string(),\n /**\n * Auto-select credentials when only one Google account is available.\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#auto_select\n */\n autoSelect: optional(boolean()),\n /**\n * The context text for the One Tap prompt.\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#context\n */\n context: optional(union([literal('signin'), literal('signup'), literal('use')])),\n /**\n * Enable FedCM (Federated Credential Management) API support. Mandatory from August 2025.\n * @see https://developers.google.com/identity/gsi/web/guides/fedcm-migration\n */\n useFedcmForPrompt: optional(boolean()),\n /**\n * Cancel the One Tap prompt if the user clicks outside.\n * @default true\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#cancel_on_tap_outside\n */\n cancelOnTapOutside: optional(boolean()),\n /**\n * The UX mode for the sign-in flow.\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#ux_mode\n */\n uxMode: optional(union([literal('popup'), literal('redirect')])),\n /**\n * The URI to redirect to after sign-in when using redirect UX mode.\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#login_uri\n */\n loginUri: optional(string()),\n /**\n * Enable Intelligent Tracking Prevention (ITP) support for Safari.\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#itp_support\n */\n itpSupport: optional(boolean()),\n /**\n * Allowed parent origin(s) for iframe embedding.\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#allowed_parent_origin\n */\n allowedParentOrigin: optional(union([string(), array(string())])),\n /**\n * Restrict sign-in to a specific Google Workspace hosted domain.\n * @example 'example.com'\n * @see https://developers.google.com/identity/gsi/web/reference/js-reference#hd\n */\n hd: optional(string()),\n})"
455+
},
456+
{
457+
"name": "GoogleSignInHelpers",
458+
"kind": "interface",
459+
"code": "export interface GoogleSignInHelpers {\n /**\n * Initialize Google Identity Services. Schema options are used as defaults;\n * pass a callback (and any other non-serializable config) here. Subsequent\n * calls are a no-op so this is safe to invoke from a remounting component.\n */\n initialize: (config?: Partial<IdConfiguration>) => void\n /**\n * Render a personalized Google Sign-In button. Auto-initializes if needed.\n * Safe to re-render on navigation or locale change.\n */\n renderButton: (parent: HTMLElement, config?: GsiButtonConfiguration) => void\n /**\n * Show the One Tap prompt. Auto-initializes if needed.\n */\n prompt: (listener?: (notification: MomentNotification) => void) => void\n}"
455460
}
456461
],
457462
"google-tag-manager": [

0 commit comments

Comments
 (0)