diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/__tests__/isPromptSkipped.test.ts b/packages/clerk-js/src/ui/components/GoogleOneTap/__tests__/isPromptSkipped.test.ts new file mode 100644 index 00000000000..1a49f6b35b5 --- /dev/null +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/__tests__/isPromptSkipped.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { isPromptSkipped } from '../one-tap-start'; + +describe('isPromptSkipped', () => { + it('returns true when isSkippedMoment returns true', () => { + expect(isPromptSkipped({ isSkippedMoment: () => true })).toBe(true); + }); + + it('returns true when getMomentType returns skipped', () => { + expect(isPromptSkipped({ getMomentType: () => 'skipped' })).toBe(true); + }); + + it('returns false when getMomentType returns dismissed', () => { + expect(isPromptSkipped({ getMomentType: () => 'dismissed' })).toBe(false); + }); + + it('returns false when no methods exist', () => { + expect(isPromptSkipped({})).toBe(false); + }); + + it('prioritizes isSkippedMoment over getMomentType', () => { + expect( + isPromptSkipped({ + isSkippedMoment: () => true, + getMomentType: () => 'dismissed', + }), + ).toBe(true); + }); +}); diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index ce4f5a97c2b..a837a210ffd 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -4,12 +4,45 @@ import { useEffect, useRef } from 'react'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { clerkUnsupportedEnvironmentWarning } from '../../../core/errors'; -import type { GISCredentialResponse } from '../../../utils/one-tap'; +import type { GISCredentialResponse, PromptMomentNotification } from '../../../utils/one-tap'; import { loadGIS } from '../../../utils/one-tap'; import { useEnvironment, useGoogleOneTapContext } from '../../contexts'; import { useFetch } from '../../hooks'; import { useRouter } from '../../router'; +/** + * Checks if the Google One Tap prompt was skipped by the user. + * Uses FedCM-compatible methods with fallback to legacy methods for backward compatibility. + * + * Per FedCM migration guide, isSkippedMoment() continues to work with FedCM, + * while getMomentType() may be removed in future Chrome versions. + * + * @see https://developers.google.com/identity/gsi/web/guides/fedcm-migration + */ +export function isPromptSkipped(notification: PromptMomentNotification): boolean { + console.log('[Clerk Debug] isPromptSkipped called with notification:', { + hasIsSkippedMoment: 'isSkippedMoment' in notification, + hasGetMomentType: 'getMomentType' in notification, + }); + + // Prioritize FedCM-compatible method + if ('isSkippedMoment' in notification) { + const result = notification.isSkippedMoment?.() ?? false; + console.log('[Clerk Debug] Using isSkippedMoment(), result:', result); + return result; + } + + // Fallback to legacy method only if FedCM method doesn't exist + if ('getMomentType' in notification) { + const result = notification.getMomentType?.() === 'skipped'; + console.log('[Clerk Debug] Using getMomentType() fallback, result:', result); + return result; + } + + console.log('[Clerk Debug] No skip detection method available, returning false'); + return false; +} + function OneTapStartInternal(): JSX.Element | null { const clerk = useClerk(); const { user } = useUser(); @@ -20,14 +53,16 @@ function OneTapStartInternal(): JSX.Element | null { const ctx = useGoogleOneTapContext(); async function oneTapCallback(response: GISCredentialResponse) { + console.log('[Clerk Debug] oneTapCallback called'); isPromptedRef.current = false; try { const res = await clerk.authenticateWithGoogleOneTap({ token: response.credential, }); + console.log('[Clerk Debug] Authentication successful'); await clerk.handleGoogleOneTapCallback(res, ctx.generateCallbackUrls(window.location.href), navigate); } catch (e) { - console.error(e); + console.error('[Clerk Debug] Authentication error:', e); } } @@ -40,7 +75,18 @@ function OneTapStartInternal(): JSX.Element | null { return undefined; } + console.log('[Clerk Debug] Loading Google Identity Services...'); const google = await loadGIS(); + console.log('[Clerk Debug] Google Identity Services loaded'); + + // TODO: Temporarily disable FedCM to test if it's causing the CORS error + const useFedCm = ctx.fedCmSupport ?? false; + console.log('[Clerk Debug] Initializing Google One Tap with:', { + fedCmSupport: ctx.fedCmSupport, + use_fedcm_for_prompt: useFedCm, + itp_support: ctx.itpSupport, + cancel_on_tap_outside: ctx.cancelOnTapOutside, + }); google.accounts.id.initialize({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -49,9 +95,11 @@ function OneTapStartInternal(): JSX.Element | null { itp_support: ctx.itpSupport, cancel_on_tap_outside: ctx.cancelOnTapOutside, auto_select: false, - use_fedcm_for_prompt: ctx.fedCmSupport, + // Default to true if not explicitly set (per the type definition) + use_fedcm_for_prompt: useFedCm, }); + console.log('[Clerk Debug] Google One Tap initialized successfully'); return google; } @@ -65,13 +113,9 @@ function OneTapStartInternal(): JSX.Element | null { useEffect(() => { if (initializedGoogle && !user?.id && !isPromptedRef.current) { - initializedGoogle.accounts.id.prompt(notification => { - // Close the modal, when the user clicks outside the prompt or cancels - if (notification.getMomentType() === 'skipped') { - // Unmounts the component will cause the useEffect cleanup function from below to be called - clerk.closeGoogleOneTap(); - } - }); + console.log('[Clerk Debug] Showing Google One Tap prompt...'); + // @ts-expect-error: testing + initializedGoogle.accounts.id.prompt(); isPromptedRef.current = true; } }, [clerk, initializedGoogle, user?.id]); @@ -80,6 +124,7 @@ function OneTapStartInternal(): JSX.Element | null { useEffect(() => { return () => { if (initializedGoogle && isPromptedRef.current) { + console.log('[Clerk Debug] Cleanup: Cancelling Google One Tap prompt'); isPromptedRef.current = false; initializedGoogle.accounts.id.cancel(); } diff --git a/packages/clerk-js/src/utils/one-tap.ts b/packages/clerk-js/src/utils/one-tap.ts index 4b56a2b083a..827695f1dbf 100644 --- a/packages/clerk-js/src/utils/one-tap.ts +++ b/packages/clerk-js/src/utils/one-tap.ts @@ -16,7 +16,19 @@ interface InitializeProps { } interface PromptMomentNotification { - getMomentType: () => 'display' | 'skipped' | 'dismissed'; + /** + * FedCM-compatible method to check if the prompt was skipped. + * This method continues to work with FedCM enabled. + * @see https://developers.google.com/identity/gsi/web/guides/fedcm-migration + */ + isSkippedMoment?: () => boolean; + /** + * Legacy method to get the moment type. + * @deprecated This method may be removed when FedCM becomes mandatory in Chrome. + * Use isSkippedMoment() instead for forward compatibility. + * @see https://developers.google.com/identity/gsi/web/guides/fedcm-migration + */ + getMomentType?: () => 'display' | 'skipped' | 'dismissed'; } interface OneTapMethods { @@ -52,4 +64,4 @@ async function loadGIS() { } export { loadGIS }; -export type { GISCredentialResponse }; +export type { GISCredentialResponse, PromptMomentNotification };