From 98b0c41218844fe28f49b389f237e6ffe5d379bd Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 4 Feb 2026 21:44:04 -0800 Subject: [PATCH 1/4] chore: initial test fix --- .../components/GoogleOneTap/one-tap-start.tsx | 15 +++++++++-- packages/clerk-js/src/utils/one-tap.ts | 25 +++++++++++++++++-- packages/shared/src/types/clerk.ts | 12 ++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) 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..7cd7c22315d 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 @@ -66,8 +66,19 @@ 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') { + // Support both FedCM and legacy modes + let shouldClose = false; + + // FedCM-compatible check (preferred) + if ('isSkippedMoment' in notification && typeof notification.isSkippedMoment === 'function') { + shouldClose = notification.isSkippedMoment(); + } + // Legacy check (fallback for non-FedCM browsers) + else if ('getMomentType' in notification && typeof notification.getMomentType === 'function') { + shouldClose = notification.getMomentType() === 'skipped'; + } + + if (shouldClose) { // Unmounts the component will cause the useEffect cleanup function from below to be called clerk.closeGoogleOneTap(); } diff --git a/packages/clerk-js/src/utils/one-tap.ts b/packages/clerk-js/src/utils/one-tap.ts index 4b56a2b083a..45c4c31194d 100644 --- a/packages/clerk-js/src/utils/one-tap.ts +++ b/packages/clerk-js/src/utils/one-tap.ts @@ -15,13 +15,34 @@ interface InitializeProps { use_fedcm_for_prompt?: boolean; } +// Legacy (deprecated in FedCM mode) interface PromptMomentNotification { - getMomentType: () => 'display' | 'skipped' | 'dismissed'; + getMomentType?: () => 'display' | 'skipped' | 'dismissed'; } +// FedCM-compatible +interface FedCMNotification { + isDisplayed?: () => boolean; + isNotDisplayed?: () => boolean; + getNotDisplayedReason?: () => + | 'browser_not_supported' + | 'invalid_client' + | 'missing_client_id' + | 'opt_out_or_no_session' + | 'secure_http_required' + | 'suppressed_by_user' + | 'unregistered_origin' + | 'unknown_reason'; + isSkippedMoment?: () => boolean; + getSkippedReason?: () => 'auto_cancel' | 'user_cancel' | 'tap_outside' | 'issuing_failed'; +} + +// Unified type supporting both +type PromptNotification = PromptMomentNotification & FedCMNotification; + interface OneTapMethods { initialize: (params: InitializeProps) => void; - prompt: (promptListener: (promptMomentNotification: PromptMomentNotification) => void) => void; + prompt: (promptListener: (promptMomentNotification: PromptNotification) => void) => void; cancel: () => void; } diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index e18204bed10..7758faef05b 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -1518,10 +1518,16 @@ export type GoogleOneTapProps = GoogleOneTapRedirectUrlProps & { */ itpSupport?: boolean; /** - * FedCM enables more private sign-in flows without requiring the use of third-party cookies. - * The browser controls user settings, displays user prompts, and only contacts an Identity Provider such as Google after explicit user consent is given. - * Backwards compatible with browsers that still support third-party cookies. + * FedCM enables more private sign-in flows without requiring third-party cookies. + * The browser controls user settings, displays user prompts, and only contacts + * an Identity Provider such as Google after explicit user consent. * + * When enabled: + * - Uses browser-native FedCM API (Chrome 116+, Edge 116+) + * - Falls back to legacy mode in unsupported browsers + * - Requires HTTPS in production + * + * @see https://developers.google.com/identity/gsi/web/guides/fedcm-migration * @default true */ fedCmSupport?: boolean; From 3eb09fdaadc319146d065c693e17cb7dd59f8297 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 4 Feb 2026 21:45:05 -0800 Subject: [PATCH 2/4] Update changeset to remove fix message --- .changeset/witty-carpets-wait.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/witty-carpets-wait.md diff --git a/.changeset/witty-carpets-wait.md b/.changeset/witty-carpets-wait.md new file mode 100644 index 00000000000..31de091758d --- /dev/null +++ b/.changeset/witty-carpets-wait.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-js": patch +"@clerk/shared": patch +--- +WIP ignore From 15a2f0147860b782191fa12e0b679b24a5fe0513 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 5 Feb 2026 13:27:01 -0800 Subject: [PATCH 3/4] more tests --- .../components/GoogleOneTap/one-tap-start.tsx | 25 +++++++++---------- packages/clerk-js/src/utils/one-tap.ts | 21 ++++++---------- packages/shared/src/types/clerk.ts | 13 +++++++--- 3 files changed, 28 insertions(+), 31 deletions(-) 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 7cd7c22315d..8338a3b79f7 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 @@ -66,21 +66,20 @@ function OneTapStartInternal(): JSX.Element | null { useEffect(() => { if (initializedGoogle && !user?.id && !isPromptedRef.current) { initializedGoogle.accounts.id.prompt(notification => { - // Support both FedCM and legacy modes - let shouldClose = false; - - // FedCM-compatible check (preferred) - if ('isSkippedMoment' in notification && typeof notification.isSkippedMoment === 'function') { - shouldClose = notification.isSkippedMoment(); + // Use isDismissedMoment() which works in both FedCM and legacy modes + // This avoids deprecation warnings about getMomentType() and isSkippedMoment() + if ('isDismissedMoment' in notification && typeof notification.isDismissedMoment === 'function') { + if (notification.isDismissedMoment()) { + // User dismissed or cancelled the prompt + clerk.closeGoogleOneTap(); + } } - // Legacy check (fallback for non-FedCM browsers) + // Fallback for legacy mode (browsers that don't support FedCM yet) else if ('getMomentType' in notification && typeof notification.getMomentType === 'function') { - shouldClose = notification.getMomentType() === 'skipped'; - } - - if (shouldClose) { - // Unmounts the component will cause the useEffect cleanup function from below to be called - clerk.closeGoogleOneTap(); + const momentType = notification.getMomentType(); + if (momentType === 'skipped' || momentType === 'dismissed') { + clerk.closeGoogleOneTap(); + } } }); isPromptedRef.current = true; diff --git a/packages/clerk-js/src/utils/one-tap.ts b/packages/clerk-js/src/utils/one-tap.ts index 45c4c31194d..c8235a7fb77 100644 --- a/packages/clerk-js/src/utils/one-tap.ts +++ b/packages/clerk-js/src/utils/one-tap.ts @@ -20,21 +20,12 @@ interface PromptMomentNotification { getMomentType?: () => 'display' | 'skipped' | 'dismissed'; } -// FedCM-compatible +// FedCM-compatible (dismissed moment still works) interface FedCMNotification { - isDisplayed?: () => boolean; - isNotDisplayed?: () => boolean; - getNotDisplayedReason?: () => - | 'browser_not_supported' - | 'invalid_client' - | 'missing_client_id' - | 'opt_out_or_no_session' - | 'secure_http_required' - | 'suppressed_by_user' - | 'unregistered_origin' - | 'unknown_reason'; + isDismissedMoment?: () => boolean; + getDismissedReason?: () => 'credential_returned' | 'cancel' | 'flow_restarted' | 'tap_outside' | 'user_cancel'; + // Note: isSkippedMoment works but without detailed reasons isSkippedMoment?: () => boolean; - getSkippedReason?: () => 'auto_cancel' | 'user_cancel' | 'tap_outside' | 'issuing_failed'; } // Unified type supporting both @@ -63,7 +54,9 @@ declare global { async function loadGIS() { if (!window.google) { try { - await loadScript('https://accounts.google.com/gsi/client', { defer: true }); + // Add timestamp to prevent caching issues with updated FedCM endpoints + const cacheBuster = `?cb=${Date.now()}`; + await loadScript(`https://accounts.google.com/gsi/client${cacheBuster}`, { defer: true }); } catch { // Rethrow with specific message clerkFailedToLoadThirdPartyScript('Google Identity Services'); diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 7758faef05b..7bdb9e55a3e 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -1522,10 +1522,15 @@ export type GoogleOneTapProps = GoogleOneTapRedirectUrlProps & { * The browser controls user settings, displays user prompts, and only contacts * an Identity Provider such as Google after explicit user consent. * - * When enabled: - * - Uses browser-native FedCM API (Chrome 116+, Edge 116+) - * - Falls back to legacy mode in unsupported browsers - * - Requires HTTPS in production + * Requirements: + * - Chrome 117+ or Edge 117+ (falls back to legacy mode in older browsers) + * - HTTPS in production + * - Proper Content Security Policy (CSP) configuration + * + * Troubleshooting: + * - If you see "NetworkError: Failed to execute 'get'" or CORS errors, check your CSP headers + * - Add `connect-src https://accounts.google.com` to your CSP policy + * - Set to `false` to temporarily use legacy mode while debugging * * @see https://developers.google.com/identity/gsi/web/guides/fedcm-migration * @default true From 85be493e5ad786c1691a96fe36cd955a85917eb9 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 5 Feb 2026 14:07:07 -0800 Subject: [PATCH 4/4] chore: fedcm compatibility --- .../components/GoogleOneTap/one-tap-start.tsx | 19 +++------ packages/clerk-js/src/utils/one-tap.ts | 39 ++++++++++++------- 2 files changed, 30 insertions(+), 28 deletions(-) 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 8338a3b79f7..418bfbd1a88 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 @@ -66,20 +66,11 @@ function OneTapStartInternal(): JSX.Element | null { useEffect(() => { if (initializedGoogle && !user?.id && !isPromptedRef.current) { initializedGoogle.accounts.id.prompt(notification => { - // Use isDismissedMoment() which works in both FedCM and legacy modes - // This avoids deprecation warnings about getMomentType() and isSkippedMoment() - if ('isDismissedMoment' in notification && typeof notification.isDismissedMoment === 'function') { - if (notification.isDismissedMoment()) { - // User dismissed or cancelled the prompt - clerk.closeGoogleOneTap(); - } - } - // Fallback for legacy mode (browsers that don't support FedCM yet) - else if ('getMomentType' in notification && typeof notification.getMomentType === 'function') { - const momentType = notification.getMomentType(); - if (momentType === 'skipped' || momentType === 'dismissed') { - clerk.closeGoogleOneTap(); - } + // Close Google One Tap when user dismisses or skips the prompt + // Using methods compatible with both FedCM and legacy modes + // https://developers.google.com/identity/gsi/web/guides/fedcm-migration + if (notification.isSkippedMoment?.() || notification.isDismissedMoment?.()) { + clerk.closeGoogleOneTap(); } }); isPromptedRef.current = true; diff --git a/packages/clerk-js/src/utils/one-tap.ts b/packages/clerk-js/src/utils/one-tap.ts index c8235a7fb77..5acedaaaa7b 100644 --- a/packages/clerk-js/src/utils/one-tap.ts +++ b/packages/clerk-js/src/utils/one-tap.ts @@ -15,25 +15,38 @@ interface InitializeProps { use_fedcm_for_prompt?: boolean; } -// Legacy (deprecated in FedCM mode) +// PromptMomentNotification methods +// See: https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification interface PromptMomentNotification { + // Moment type detection getMomentType?: () => 'display' | 'skipped' | 'dismissed'; -} -// FedCM-compatible (dismissed moment still works) -interface FedCMNotification { - isDismissedMoment?: () => boolean; - getDismissedReason?: () => 'credential_returned' | 'cancel' | 'flow_restarted' | 'tap_outside' | 'user_cancel'; - // Note: isSkippedMoment works but without detailed reasons + // Display moment methods - NOT supported when FedCM is enabled + isDisplayMoment?: () => boolean; + isDisplayed?: () => boolean; + isNotDisplayed?: () => boolean; + getNotDisplayedReason?: () => + | 'browser_not_supported' + | 'invalid_client' + | 'missing_client_id' + | 'opt_out_or_no_session' + | 'secure_http_required' + | 'suppressed_by_user' + | 'unregistered_origin' + | 'unknown_reason'; + + // Skipped moment methods - partially supported in FedCM (no user_cancel reason) isSkippedMoment?: () => boolean; -} + getSkippedReason?: () => 'auto_cancel' | 'user_cancel' | 'tap_outside' | 'issuing_failed'; -// Unified type supporting both -type PromptNotification = PromptMomentNotification & FedCMNotification; + // Dismissed moment methods - fully supported in FedCM + isDismissedMoment?: () => boolean; + getDismissedReason?: () => 'credential_returned' | 'cancel_called' | 'flow_restarted'; +} interface OneTapMethods { initialize: (params: InitializeProps) => void; - prompt: (promptListener: (promptMomentNotification: PromptNotification) => void) => void; + prompt: (promptListener?: (notification: PromptMomentNotification) => void) => void; cancel: () => void; } @@ -54,9 +67,7 @@ declare global { async function loadGIS() { if (!window.google) { try { - // Add timestamp to prevent caching issues with updated FedCM endpoints - const cacheBuster = `?cb=${Date.now()}`; - await loadScript(`https://accounts.google.com/gsi/client${cacheBuster}`, { defer: true }); + await loadScript('https://accounts.google.com/gsi/client', { defer: true }); } catch { // Rethrow with specific message clerkFailedToLoadThirdPartyScript('Google Identity Services');