Skip to content

Commit 9b7556e

Browse files
committed
refactor: init google one tap flow
1 parent f963529 commit 9b7556e

1 file changed

Lines changed: 90 additions & 39 deletions

File tree

packages/webapp/components/GoogleOneTapAuth.tsx

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import Script from 'next/script'
22
import { createClient } from '@utils/supabase/component'
33
import { CredentialResponse } from 'google-one-tap'
44
import { useRouter } from 'next/router'
5-
import { useEffect, useCallback } from 'react'
5+
import { useEffect, useCallback, useRef } from 'react'
66

77
const OneTapComponent = () => {
88
const supabase = createClient()
99
const router = useRouter()
10+
const initializationRef = useRef(false)
11+
const abortControllerRef = useRef<AbortController | null>(null)
1012

1113
const handleCredentialResponse = useCallback(
1214
async (response: CredentialResponse) => {
@@ -39,64 +41,113 @@ const OneTapComponent = () => {
3941
[supabase.auth, router]
4042
)
4143

42-
useEffect(() => {
43-
const initializeGoogleOneTap = async () => {
44-
try {
45-
// Check session first
46-
const {
47-
data: { session },
48-
error: sessionError
49-
} = await supabase.auth.getSession()
44+
const initializeGoogleOneTap = useCallback(async () => {
45+
// Prevent multiple initializations
46+
if (initializationRef.current) return
47+
48+
try {
49+
// Create AbortController for this operation
50+
const abortController = new AbortController()
51+
abortControllerRef.current = abortController
52+
53+
// Check if component is still mounted
54+
if (abortController.signal.aborted) return
55+
56+
// Check session first
57+
const {
58+
data: { session },
59+
error: sessionError
60+
} = await supabase.auth.getSession()
61+
62+
if (sessionError) throw sessionError
63+
if (session) return
5064

51-
if (sessionError) throw sessionError
52-
if (session) return
65+
if (!process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
66+
console.error('NEXT_PUBLIC_GOOGLE_CLIENT_ID is not set')
67+
return
68+
}
69+
70+
// Wait for Google script with retry mechanism
71+
let attempts = 0
72+
const maxAttempts = 20 // 4 seconds max wait
73+
74+
while (attempts < maxAttempts) {
75+
if (abortController.signal.aborted) return
76+
77+
if (typeof window.google !== 'undefined' && window.google.accounts) {
78+
break
79+
}
5380

54-
if (typeof window.google === 'undefined') {
55-
console.error('Google script not loaded')
81+
attempts++
82+
if (attempts >= maxAttempts) {
83+
console.error('Google script failed to load after 4 seconds')
5684
return
5785
}
5886

59-
window.google.accounts.id.initialize({
60-
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
61-
callback: handleCredentialResponse,
62-
auto_select: false, // Don't auto select the account
63-
cancel_on_tap_outside: true,
64-
use_fedcm_for_prompt: true
65-
})
66-
67-
window.google.accounts.id.prompt((notification: any) => {
68-
if (notification.isNotDisplayed()) {
69-
console.info('One Tap not displayed:', notification.getNotDisplayedReason())
70-
} else if (notification.isSkippedMoment()) {
71-
console.info('One Tap skipped:', notification.getSkippedReason())
72-
}
73-
})
74-
} catch (error) {
75-
console.error('Error initializing Google One Tap:', error)
87+
await new Promise((resolve) => setTimeout(resolve, 200))
7688
}
77-
}
7889

79-
// Small delay to ensure the script is loaded
80-
const timeoutId = setTimeout(initializeGoogleOneTap, 1000)
90+
// Check if still mounted before initializing
91+
if (abortController.signal.aborted) return
92+
93+
// Mark as initialized to prevent multiple calls
94+
initializationRef.current = true
8195

96+
window.google.accounts.id.initialize({
97+
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
98+
callback: handleCredentialResponse,
99+
auto_select: false,
100+
cancel_on_tap_outside: true,
101+
use_fedcm_for_prompt: true
102+
})
103+
104+
window.google.accounts.id.prompt((notification: any) => {
105+
if (notification.isNotDisplayed()) {
106+
console.info('One Tap not displayed:', notification.getNotDisplayedReason())
107+
} else if (notification.isSkippedMoment()) {
108+
console.info('One Tap skipped:', notification.getSkippedReason())
109+
}
110+
})
111+
} catch (error) {
112+
console.error('Error initializing Google One Tap:', error)
113+
initializationRef.current = false // Reset on error
114+
}
115+
}, [supabase.auth, handleCredentialResponse])
116+
117+
useEffect(() => {
82118
return () => {
83-
clearTimeout(timeoutId)
84-
// Cleanup
119+
// Abort any ongoing operations
120+
if (abortControllerRef.current) {
121+
abortControllerRef.current.abort()
122+
abortControllerRef.current = null
123+
}
124+
125+
// Cancel Google One Tap
85126
if (typeof window.google !== 'undefined') {
86127
window.google.accounts.id.cancel()
87128
}
129+
130+
// Reset initialization flag
131+
initializationRef.current = false
88132
}
89-
}, [handleCredentialResponse, router, supabase.auth])
133+
}, [])
90134

91135
return (
92136
<>
93137
<Script
94138
src="https://accounts.google.com/gsi/client"
95139
strategy="afterInteractive"
96-
onLoad={() => console.info('Google script loaded')}
97-
onError={(e) => console.error('Error loading Google script:', e)}
140+
onLoad={() => {
141+
// Initialize when script loads
142+
initializeGoogleOneTap()
143+
}}
144+
onError={(e) => {
145+
console.error('Failed to load Google One Tap script:', e)
146+
// Reset initialization flag so we can try again if needed
147+
initializationRef.current = false
148+
}}
98149
/>
99-
<div id="oneTap" className="fixed right-0 top-0 z-[100]" />
150+
<div id="oneTap" className="fixed top-0 right-0 z-[100]" />
100151
</>
101152
)
102153
}

0 commit comments

Comments
 (0)