Skip to content
Closed
Original file line number Diff line number Diff line change
@@ -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);
});
});
65 changes: 55 additions & 10 deletions packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
}

Expand All @@ -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
Expand All @@ -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;
}

Expand All @@ -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]);
Expand All @@ -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();
}
Expand Down
16 changes: 14 additions & 2 deletions packages/clerk-js/src/utils/one-tap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -52,4 +64,4 @@ async function loadGIS() {
}

export { loadGIS };
export type { GISCredentialResponse };
export type { GISCredentialResponse, PromptMomentNotification };
Loading