diff --git a/.changeset/funny-berries-nail.md b/.changeset/funny-berries-nail.md
new file mode 100644
index 00000000000..fd64c2d070b
--- /dev/null
+++ b/.changeset/funny-berries-nail.md
@@ -0,0 +1,14 @@
+---
+'@clerk/nextjs': minor
+'@clerk/clerk-react': minor
+---
+
+Add support for GoogleOneTap
+### React component
+- ``
+
+ Customize the UX of the prompt
+
+```tsx
+
+```
diff --git a/.changeset/mighty-dolphins-look.md b/.changeset/mighty-dolphins-look.md
new file mode 100644
index 00000000000..9132439216b
--- /dev/null
+++ b/.changeset/mighty-dolphins-look.md
@@ -0,0 +1,37 @@
+---
+'@clerk/clerk-js': minor
+---
+
+### Use the Google One Tap component from with Vanilla JS
+- `Clerk.openGoogleOneTap({ cancelOnTapOutside: false, fedCmSupport: false, itpSupport: false })`
+- `Clerk.closeGoogleOneTap()`
+### Low level APIs for custom flows
+- `await Clerk.authenticateWithGoogleOneTap({ token: 'xxxx'})`
+- `await Clerk.handleGoogleOneTapCallback()`
+
+
+We recommend using this two methods together in order and let Clerk perform the correct redirections.
+```tsx
+google.accounts.id.initialize({
+ callback: async response => {
+ const signInOrUp = await Clerk.authenticateWithGoogleOneTap({ token: response.credential})
+ await Clerk.handleGoogleOneTapCallback(signInOrUp, {
+ afterSignInUrl: window.location.href,
+ })
+ },
+});
+```
+
+In case you want to handle the redirection and session management yourself you can do so like this
+```tsx
+google.accounts.id.initialize({
+ callback: async response => {
+ const signInOrUp = await Clerk.authenticateWithGoogleOneTap({ token: response.credential})
+ if(signInOrUp.status === 'complete') {
+ await Clerk.setActive({
+ session: signInOrUp.createdSessionId
+ })
+ }
+ },
+});
+```
diff --git a/.changeset/shaggy-elephants-shop.md b/.changeset/shaggy-elephants-shop.md
new file mode 100644
index 00000000000..c7e5d2b4274
--- /dev/null
+++ b/.changeset/shaggy-elephants-shop.md
@@ -0,0 +1,5 @@
+---
+'@clerk/chrome-extension': minor
+---
+
+Update export snapshot tests to include `GoogleOneTap`.
diff --git a/.changeset/silly-mugs-type.md b/.changeset/silly-mugs-type.md
new file mode 100644
index 00000000000..a6029c59763
--- /dev/null
+++ b/.changeset/silly-mugs-type.md
@@ -0,0 +1,21 @@
+---
+'@clerk/types': minor
+---
+
+Added the following types
+```tsx
+interface Clerk {
+ ...
+ openGoogleOneTap: (props?: GoogleOneTapProps) => void;
+ closeGoogleOneTap: () => void;
+ authenticateWithGoogleOneTap: (params: AuthenticateWithGoogleOneTapParams) => Promise;
+ handleGoogleOneTapCallback: (
+ signInOrUp: SignInResource | SignUpResource,
+ params: HandleOAuthCallbackParams,
+ customNavigate?: (to: string) => Promise,
+ ) => Promise;
+ ...
+}
+
+type GoogleOneTapStrategy = 'google_one_tap'
+```
diff --git a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap
index 374c803e3af..08dbadcf503 100644
--- a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap
+++ b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap
@@ -8,6 +8,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkProvider",
"CreateOrganization",
"EmailLinkErrorCode",
+ "GoogleOneTap",
"MagicLinkErrorCode",
"MultisessionAppSupport",
"OrganizationList",
@@ -32,7 +33,6 @@ exports[`public exports should not include a breaking change 1`] = `
"WithClerk",
"WithSession",
"WithUser",
- "__experimental_GoogleOneTap",
"__internal__setErrorThrowerOptions",
"isClerkAPIResponseError",
"isEmailLinkError",
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 3bc9144f629..4a9cfe2af1e 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -5,6 +5,7 @@ import {
handleValueOrFn,
inBrowser as inClientSide,
is4xxError,
+ isClerkAPIResponseError,
isHttpOrHttps,
isLegacyFrontendApiKey,
isValidBrowserOnline,
@@ -17,6 +18,7 @@ import {
} from '@clerk/shared';
import type {
ActiveSessionResource,
+ AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
BeforeEmitCallback,
Clerk as ClerkInterface,
@@ -28,12 +30,12 @@ import type {
DomainOrProxyUrl,
EnvironmentJSON,
EnvironmentResource,
+ GoogleOneTapProps,
HandleEmailLinkVerificationParams,
HandleMagicLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
ListenerCallback,
- OneTapProps,
OrganizationInvitationResource,
OrganizationListProps,
OrganizationMembershipResource,
@@ -364,6 +366,18 @@ export default class Clerk implements ClerkInterface {
}
};
+ public openGoogleOneTap = (props?: GoogleOneTapProps): void => {
+ this.assertComponentsReady(this.#componentControls);
+ void this.#componentControls
+ .ensureMounted({ preloadHint: 'GoogleOneTap' })
+ .then(controls => controls.openModal('googleOneTap', props || {}));
+ };
+
+ public closeGoogleOneTap = (): void => {
+ this.assertComponentsReady(this.#componentControls);
+ void this.#componentControls.ensureMounted().then(controls => controls.closeModal('googleOneTap'));
+ };
+
public openSignIn = (props?: SignInProps): void => {
this.assertComponentsReady(this.#componentControls);
if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment) && this.#instanceType === 'development') {
@@ -457,30 +471,6 @@ export default class Clerk implements ClerkInterface {
);
};
- public __experimental_mountGoogleOneTap = (node: HTMLDivElement, props?: OneTapProps): void => {
- this.assertComponentsReady(this.#componentControls);
-
- void this.#componentControls.ensureMounted({ preloadHint: 'OneTap' }).then(controls =>
- controls.mountComponent({
- name: 'OneTap',
- appearanceKey: 'oneTap',
- node,
- props,
- }),
- );
- // TODO-ONETAP: Enable telemetry one feature is ready for public beta
- // this.telemetry?.record(eventComponentMounted('GoogleOneTap', props));
- };
-
- public __experimental_unmountGoogleOneTap = (node: HTMLDivElement): void => {
- this.assertComponentsReady(this.#componentControls);
- void this.#componentControls.ensureMounted().then(controls =>
- controls.unmountComponent({
- node,
- }),
- );
- };
-
public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls =>
@@ -966,14 +956,47 @@ export default class Clerk implements ClerkInterface {
return null;
};
- public handleRedirectCallback = async (
- params: HandleOAuthCallbackParams = {},
+ public handleGoogleOneTapCallback = async (
+ signInOrUp: SignInResource | SignUpResource,
+ params: HandleOAuthCallbackParams,
customNavigate?: (to: string) => Promise,
): Promise => {
if (!this.#isReady || !this.#environment || !this.client) {
return;
}
- const { signIn, signUp } = this.client;
+ const { signIn: _signIn, signUp: _signUp } = this.client;
+
+ const signIn = 'identifier' in (signInOrUp || {}) ? (signInOrUp as SignInResource) : _signIn;
+ const signUp = 'missingFields' in (signInOrUp || {}) ? (signInOrUp as SignUpResource) : _signUp;
+
+ const navigate = (to: string) =>
+ customNavigate && typeof customNavigate === 'function'
+ ? customNavigate(this.buildUrlWithAuth(to))
+ : this.navigate(this.buildUrlWithAuth(to));
+
+ return this._handleRedirectCallback(params, {
+ signUp,
+ signIn,
+ navigate,
+ });
+ };
+
+ private _handleRedirectCallback = async (
+ params: HandleOAuthCallbackParams,
+ {
+ signIn,
+ signUp,
+ navigate,
+ }: {
+ signIn: SignInResource;
+ signUp: SignUpResource;
+ navigate: (to: string) => Promise;
+ },
+ ): Promise => {
+ if (!this.loaded || !this.#environment || !this.client) {
+ return;
+ }
+
const { displayConfig } = this.#environment;
const { firstFactorVerification } = signIn;
const { externalAccount } = signUp.verifications;
@@ -983,6 +1006,7 @@ export default class Clerk implements ClerkInterface {
externalAccountStatus: externalAccount.status,
externalAccountErrorCode: externalAccount.error?.code,
externalAccountSessionId: externalAccount.error?.meta?.sessionId,
+ sessionId: signUp.createdSessionId,
};
const si = {
@@ -990,11 +1014,9 @@ export default class Clerk implements ClerkInterface {
firstFactorVerificationStatus: firstFactorVerification.status,
firstFactorVerificationErrorCode: firstFactorVerification.error?.code,
firstFactorVerificationSessionId: firstFactorVerification.error?.meta?.sessionId,
+ sessionId: signIn.createdSessionId,
};
- const navigate = (to: string) =>
- customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to);
-
const makeNavigate = (to: string) => () => navigate(to);
const navigateToSignIn = makeNavigate(displayConfig.signInUrl);
@@ -1026,7 +1048,13 @@ export default class Clerk implements ClerkInterface {
const navigateToContinueSignUp = makeNavigate(
params.continueSignUpUrl ||
- buildURL({ base: displayConfig.signUpUrl, hashPath: '/continue' }, { stringify: true }),
+ buildURL(
+ {
+ base: displayConfig.signUpUrl,
+ hashPath: '/continue',
+ },
+ { stringify: true },
+ ),
);
const navigateToNextStepSignUp = ({ missingFields }: { missingFields: SignUpField[] }) => {
@@ -1046,6 +1074,13 @@ export default class Clerk implements ClerkInterface {
});
};
+ if (si.status === 'complete') {
+ return this.setActive({
+ session: si.sessionId,
+ beforeEmit: navigateAfterSignIn,
+ });
+ }
+
const userExistsButNeedsToSignIn =
su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists';
@@ -1108,6 +1143,13 @@ export default class Clerk implements ClerkInterface {
}
}
+ if (su.status === 'complete') {
+ return this.setActive({
+ session: su.sessionId,
+ beforeEmit: navigateAfterSignUp,
+ });
+ }
+
if (si.status === 'needs_second_factor') {
return navigateToFactorTwo();
}
@@ -1144,6 +1186,25 @@ export default class Clerk implements ClerkInterface {
return navigateToSignIn();
};
+ public handleRedirectCallback = async (
+ params: HandleOAuthCallbackParams = {},
+ customNavigate?: (to: string) => Promise,
+ ): Promise => {
+ if (!this.loaded || !this.#environment || !this.client) {
+ return;
+ }
+ const { signIn, signUp } = this.client;
+
+ const navigate = (to: string) =>
+ customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to);
+
+ return this._handleRedirectCallback(params, {
+ signUp,
+ signIn,
+ navigate,
+ });
+ };
+
public handleUnauthenticated = async (opts = { broadcast: true }): Promise => {
if (!this.client || !this.session) {
return;
@@ -1159,6 +1220,25 @@ export default class Clerk implements ClerkInterface {
return this.setActive({ session: null });
};
+ public authenticateWithGoogleOneTap = async (
+ params: AuthenticateWithGoogleOneTapParams,
+ ): Promise => {
+ return this.client?.signIn
+ .create({
+ strategy: 'google_one_tap',
+ token: params.token,
+ })
+ .catch(err => {
+ if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') {
+ return this.client?.signUp.create({
+ strategy: 'google_one_tap',
+ token: params.token,
+ });
+ }
+ throw err;
+ }) as Promise;
+ };
+
public authenticateWithMetamask = async ({
redirectUrl,
signUpContinueUrl,
diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts
index 7ae41eafcdb..9df5c10d3b0 100644
--- a/packages/clerk-js/src/core/resources/SignIn.ts
+++ b/packages/clerk-js/src/core/resources/SignIn.ts
@@ -1,6 +1,5 @@
-import { deepSnakeToCamel, deprecated, isClerkAPIResponseError, Poller } from '@clerk/shared';
+import { deepSnakeToCamel, deprecated, Poller } from '@clerk/shared';
import type {
- __experimental_AuthenticateWithGoogleOneTapParams,
AttemptFirstFactorParams,
AttemptSecondFactorParams,
AuthenticateWithRedirectParams,
@@ -25,7 +24,6 @@ import type {
SignInStartEmailLinkFlowParams,
SignInStartMagicLinkFlowParams,
SignInStatus,
- SignUpResource,
VerificationResource,
Web3SignatureConfig,
Web3SignatureFactor,
@@ -240,27 +238,6 @@ export class SignIn extends BaseResource implements SignInResource {
}
};
- public __experimental_authenticateWithGoogleOneTap = async (
- params: __experimental_AuthenticateWithGoogleOneTapParams,
- ): Promise => {
- return this.create({
- // TODO-ONETAP: Add new types when feature is ready for public beta
- // @ts-expect-error
- strategy: 'google_one_tap',
- googleOneTapToken: params.token,
- }).catch(err => {
- if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') {
- return SignIn.clerk.client?.signUp.create({
- // TODO-ONETAP: Add new types when feature is ready for public beta
- // @ts-expect-error
- strategy: 'google_one_tap',
- googleOneTapToken: params.token,
- });
- }
- throw err;
- }) as Promise;
- };
-
public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => {
const { identifier, generateSignature } = params || {};
if (!(typeof generateSignature === 'function')) {
diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx
index ef8fb3a3160..2b94fa63e53 100644
--- a/packages/clerk-js/src/ui/Components.tsx
+++ b/packages/clerk-js/src/ui/Components.tsx
@@ -6,6 +6,7 @@ import type {
ClerkOptions,
CreateOrganizationProps,
EnvironmentResource,
+ GoogleOneTapProps,
OrganizationProfileProps,
SignInProps,
SignUpProps,
@@ -32,6 +33,7 @@ import {
LazyComponentRenderer,
LazyImpersonationFabProvider,
LazyModalRenderer,
+ LazyOneTapRenderer,
LazyProviders,
} from './lazyModules/providers';
import type { AvailableComponentProps } from './types';
@@ -52,11 +54,15 @@ export type ComponentControls = {
node?: HTMLDivElement;
props?: unknown;
}) => void;
- openModal: (
+ openModal: <
+ T extends 'googleOneTap' | 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization',
+ >(
modal: T,
props: T extends 'signIn' ? SignInProps : T extends 'signUp' ? SignUpProps : UserProfileProps,
) => void;
- closeModal: (modal: 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization') => void;
+ closeModal: (
+ modal: 'googleOneTap' | 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization',
+ ) => void;
// Special case, as the impersonation fab mounts automatically
mountImpersonationFab: () => void;
};
@@ -78,6 +84,7 @@ interface ComponentsProps {
interface ComponentsState {
appearance: Appearance | undefined;
options: ClerkOptions | undefined;
+ googleOneTapModal: null | GoogleOneTapProps;
signInModal: null | SignInProps;
signUpModal: null | SignUpProps;
userProfileModal: null | UserProfileProps;
@@ -153,6 +160,7 @@ const Components = (props: ComponentsProps) => {
const [state, setState] = React.useState({
appearance: props.options.appearance,
options: props.options,
+ googleOneTapModal: null,
signInModal: null,
signUpModal: null,
userProfileModal: null,
@@ -161,8 +169,15 @@ const Components = (props: ComponentsProps) => {
nodes: new Map(),
impersonationFab: false,
});
- const { signInModal, signUpModal, userProfileModal, organizationProfileModal, createOrganizationModal, nodes } =
- state;
+ const {
+ googleOneTapModal,
+ signInModal,
+ signUpModal,
+ userProfileModal,
+ organizationProfileModal,
+ createOrganizationModal,
+ nodes,
+ } = state;
const { urlStateParam, clearUrlStateParam, decodedRedirectParams } = useClerkModalStateParams();
@@ -219,6 +234,15 @@ const Components = (props: ComponentsProps) => {
props.onComponentsMounted();
}, []);
+ const mountedOneTapModal = (
+
+ );
+
const mountedSignInModal = (
{
);
})}
+ {googleOneTapModal && mountedOneTapModal}
{signInModal && mountedSignInModal}
{signUpModal && mountedSignUpModal}
{userProfileModal && mountedUserProfileModal}
diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/index.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/index.tsx
index 28c1756432a..3a755535db8 100644
--- a/packages/clerk-js/src/ui/components/GoogleOneTap/index.tsx
+++ b/packages/clerk-js/src/ui/components/GoogleOneTap/index.tsx
@@ -1,4 +1,4 @@
-import type { OneTapProps } from '@clerk/types';
+import type { GoogleOneTapProps } from '@clerk/types';
import React from 'react';
import { withCoreSessionSwitchGuard } from '../../contexts';
@@ -22,4 +22,4 @@ function OneTapRoutes(): JSX.Element {
OneTapRoutes.displayName = 'OneTap';
-export const OneTap: React.ComponentType = withCoreSessionSwitchGuard(OneTapRoutes);
+export const OneTap: React.ComponentType = withCoreSessionSwitchGuard(OneTapRoutes);
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 c06270bfa05..1822bb39bb2 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
@@ -1,82 +1,81 @@
import { useUserContext } from '@clerk/shared/react';
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
-import { clerkInvalidFAPIResponse } from '../../../core/errors';
import type { GISCredentialResponse } from '../../../utils/one-tap';
import { loadGIS } from '../../../utils/one-tap';
-import { useCoreClerk, useCoreSignIn, useEnvironment, useGoogleOneTapContext } from '../../contexts';
+import { useCoreClerk, useEnvironment, useGoogleOneTapContext } from '../../contexts';
import { withCardStateProvider } from '../../elements';
import { useFetch } from '../../hooks';
-import { useSupportEmail } from '../../hooks/useSupportEmail';
+import { useRouter } from '../../router';
function _OneTapStart(): JSX.Element | null {
const clerk = useCoreClerk();
- const signIn = useCoreSignIn();
const user = useUserContext();
const environment = useEnvironment();
+ const isPromptedRef = useRef(false);
+ const { navigate } = useRouter();
- const supportEmail = useSupportEmail();
const ctx = useGoogleOneTapContext();
async function oneTapCallback(response: GISCredentialResponse) {
+ isPromptedRef.current = false;
try {
- const res = await signIn.__experimental_authenticateWithGoogleOneTap({
+ const res = await clerk.authenticateWithGoogleOneTap({
token: response.credential,
});
-
- switch (res.status) {
- case 'complete':
- await clerk.setActive({
- session: res.createdSessionId,
- });
- break;
- // TODO-ONETAP: Add a new case in order to handle the `missing_requirements` status and the PSU flow
- default:
- clerkInvalidFAPIResponse(res.status, supportEmail);
- break;
- }
- } catch (err) {
- /**
- * Currently it is not possible to display an error in the UI.
- * As a fallback we simply open the SignIn modal for the user to sign in.
- */
- clerk.openSignIn();
+ await clerk.handleGoogleOneTapCallback(res, ctx.generateCallbackUrls(window.location.href), navigate);
+ } catch (e) {
+ console.error(e);
}
}
const environmentClientID = environment.displayConfig.googleOneTapClientId;
const shouldLoadGIS = !user?.id && !!environmentClientID;
+ async function initializeGIS() {
+ const google = await loadGIS();
+ google.accounts.id.initialize({
+ client_id: environmentClientID!,
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ callback: oneTapCallback,
+ itp_support: ctx.itpSupport,
+ cancel_on_tap_outside: ctx.cancelOnTapOutside,
+ auto_select: false,
+ use_fedcm_for_prompt: ctx.fedCmSupport,
+ });
+ return google;
+ }
+
/**
* Prevent GIS from initializing multiple times
*/
- const { data: google } = useFetch(shouldLoadGIS ? loadGIS : undefined, 'google-identity-services-script', {
- onSuccess(google) {
- google.accounts.id.initialize({
- client_id: environmentClientID!,
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- callback: oneTapCallback,
- itp_support: true,
- cancel_on_tap_outside: ctx.cancelOnTapOutside,
- auto_select: false,
- use_fedcm_for_prompt: true,
- });
+ const { data: initializedGoogle } = useFetch(
+ shouldLoadGIS ? initializeGIS : undefined,
+ 'google-identity-services-script',
+ );
- google.accounts.id.prompt();
- },
- });
+ 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();
+ }
+ });
+ isPromptedRef.current = true;
+ }
+ }, [clerk, initializedGoogle, user?.id]);
// Trigger only on mount/unmount. Above we handle the logic for the initial fetch + initialization
useEffect(() => {
- if (google && !user?.id) {
- google.accounts.id.prompt();
- }
return () => {
- if (google) {
- google.accounts.id.cancel();
+ if (initializedGoogle && isPromptedRef.current) {
+ isPromptedRef.current = false;
+ initializedGoogle.accounts.id.cancel();
}
};
- }, [user?.id]);
+ }, [initializedGoogle]);
return null;
}
diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
index 98df5652859..3726ca4e254 100644
--- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
@@ -1,6 +1,6 @@
import { deprecated, snakeToCamel } from '@clerk/shared';
-import type { OrganizationResource, UserResource } from '@clerk/types';
-import React, { useMemo } from 'react';
+import type { HandleOAuthCallbackParams, OrganizationResource, UserResource } from '@clerk/types';
+import React, { useCallback, useMemo } from 'react';
import { SIGN_IN_INITIAL_VALUE_KEYS, SIGN_UP_INITIAL_VALUE_KEYS } from '../../core/constants';
import { buildAuthQueryString, buildURL, createDynamicParamParser, pickRedirectionProp } from '../../utils';
@@ -12,7 +12,7 @@ import { useRouter } from '../router';
import type {
AvailableComponentCtx,
CreateOrganizationCtx,
- OneTapCtx,
+ GoogleOneTapCtx,
OrganizationListCtx,
OrganizationProfileCtx,
OrganizationSwitcherCtx,
@@ -491,14 +491,91 @@ export const useCreateOrganizationContext = () => {
};
export const useGoogleOneTapContext = () => {
- const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as OneTapCtx;
+ const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as GoogleOneTapCtx;
+ const options = useOptions();
+ const { displayConfig } = useEnvironment();
+ const { queryParams } = useRouter();
+ const clerk = useCoreClerk();
- if (componentName !== 'OneTap') {
+ if (componentName !== 'GoogleOneTap') {
throw new Error('Clerk: useGoogleOneTapContext called outside GoogleOneTap.');
}
+ const generateCallbackUrls = useCallback(
+ (returnBackUrl: string): HandleOAuthCallbackParams => {
+ const signInUrl = pickRedirectionProp('signInUrl', { options, displayConfig }, false);
+ const signUpUrl = pickRedirectionProp('signUpUrl', { options, displayConfig }, false);
+
+ const afterSignUpUrl = clerk.buildUrlWithAuth(
+ pickRedirectionProp('afterSignUpUrl', {
+ queryParams,
+ ctx: {
+ ...ctx,
+ afterSignUpUrl: returnBackUrl,
+ },
+ options,
+ displayConfig,
+ }),
+ );
+
+ const afterSignInUrl = clerk.buildUrlWithAuth(
+ pickRedirectionProp('afterSignInUrl', {
+ queryParams,
+ ctx: {
+ ...ctx,
+ afterSignInUrl: returnBackUrl,
+ },
+ options,
+ displayConfig,
+ }),
+ );
+
+ const signUpContinueUrl = buildURL(
+ {
+ base: signUpUrl,
+ hashPath: '/continue',
+ hashSearch: new URLSearchParams({
+ after_sign_up_url: afterSignUpUrl,
+ }).toString(),
+ },
+ { stringify: true },
+ );
+
+ const firstFactorUrl = buildURL(
+ {
+ base: signInUrl,
+ hashPath: '/factor-one',
+ hashSearch: new URLSearchParams({
+ after_sign_in_url: afterSignInUrl,
+ }).toString(),
+ },
+ { stringify: true },
+ );
+ const secondFactorUrl = buildURL(
+ {
+ base: signInUrl,
+ hashPath: '/factor-two',
+ hashSearch: new URLSearchParams({
+ after_sign_in_url: afterSignInUrl,
+ }).toString(),
+ },
+ { stringify: true },
+ );
+
+ return {
+ firstFactorUrl,
+ secondFactorUrl,
+ continueSignUpUrl: signUpContinueUrl,
+ afterSignUpUrl,
+ afterSignInUrl,
+ };
+ },
+ [ctx, displayConfig.signInUrl, displayConfig.signUpUrl, options, queryParams],
+ );
+
return {
...ctx,
componentName,
+ generateCallbackUrls,
};
};
diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts
index ed3cf529350..5cac14588f9 100644
--- a/packages/clerk-js/src/ui/lazyModules/components.ts
+++ b/packages/clerk-js/src/ui/lazyModules/components.ts
@@ -12,13 +12,15 @@ const componentImportPaths = {
import(/* webpackChunkName: "organizationswitcher" */ './../components/OrganizationSwitcher'),
OrganizationList: () => import(/* webpackChunkName: "organizationlist" */ './../components/OrganizationList'),
ImpersonationFab: () => import(/* webpackChunkName: "impersonationfab" */ './../components/ImpersonationFab'),
- OneTap: () => import(/* webpackChunkName: "oneTap" */ './../components/GoogleOneTap'),
+ GoogleOneTap: () => import(/* webpackChunkName: "oneTap" */ './../components/GoogleOneTap'),
} as const;
export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn })));
export const SignInModal = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignInModal })));
-export const OneTap = lazy(() => componentImportPaths.OneTap().then(module => ({ default: module.OneTap })));
+export const GoogleOneTap = lazy(() =>
+ componentImportPaths.GoogleOneTap().then(module => ({ default: module.OneTap })),
+);
export const SignUp = lazy(() => componentImportPaths.SignUp().then(module => ({ default: module.SignUp })));
@@ -79,7 +81,7 @@ export const ClerkComponents = {
UserProfileModal,
OrganizationProfileModal,
CreateOrganizationModal,
- OneTap,
+ GoogleOneTap,
};
export type ClerkComponentName = keyof typeof ClerkComponents;
diff --git a/packages/clerk-js/src/ui/lazyModules/providers.tsx b/packages/clerk-js/src/ui/lazyModules/providers.tsx
index a76e0ac432f..74244e232d6 100644
--- a/packages/clerk-js/src/ui/lazyModules/providers.tsx
+++ b/packages/clerk-js/src/ui/lazyModules/providers.tsx
@@ -12,7 +12,8 @@ const OptionsProvider = lazy(() => import('../contexts').then(m => ({ default: m
const AppearanceProvider = lazy(() => import('../customizables').then(m => ({ default: m.AppearanceProvider })));
const VirtualRouter = lazy(() => import('../router').then(m => ({ default: m.VirtualRouter })));
const InternalThemeProvider = lazy(() => import('../styledSystem').then(m => ({ default: m.InternalThemeProvider })));
-const Portal = lazy(() => import('./../portal'));
+const Portal = lazy(() => import('./../portal').then(m => ({ default: m.Portal })));
+const VirtualBodyRootPortal = lazy(() => import('./../portal').then(m => ({ default: m.VirtualBodyRootPortal })));
const FlowMetadataProvider = lazy(() => import('./../elements').then(m => ({ default: m.FlowMetadataProvider })));
const Modal = lazy(() => import('./../elements').then(m => ({ default: m.Modal })));
@@ -128,3 +129,27 @@ export const LazyImpersonationFabProvider = (
);
};
+
+type LazyOneTapRendererProps = React.PropsWithChildren<
+ {
+ componentProps: any;
+ startPath: string;
+ } & Omit
+>;
+
+export const LazyOneTapRenderer = (props: LazyOneTapRendererProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/clerk-js/src/ui/portal/index.tsx b/packages/clerk-js/src/ui/portal/index.tsx
index 4ea9078f54b..4190079f47c 100644
--- a/packages/clerk-js/src/ui/portal/index.tsx
+++ b/packages/clerk-js/src/ui/portal/index.tsx
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
import { PRESERVED_QUERYSTRING_PARAMS } from '../../core/constants';
import { clerkErrorPathRouterMissingPath } from '../../core/errors';
-import { buildVirtualRouterUrl } from '../../utils';
import { ComponentContext } from '../contexts';
import { HashRouter, PathRouter, VirtualRouter } from '../router';
import type { AvailableComponentCtx } from '../types';
@@ -15,21 +14,7 @@ type PortalProps;
-export default class Portal extends React.PureComponent> {
- private elRef = document.createElement('div');
-
- componentDidMount() {
- if (this.props.componentName === 'OneTap') {
- document.body.appendChild(this.elRef);
- }
- }
-
- componentWillUnmount() {
- if (this.props.componentName === 'OneTap') {
- document.body.removeChild(this.elRef);
- }
- }
-
+export class Portal extends React.PureComponent> {
render() {
const { props, component, componentName, node } = this.props;
@@ -39,13 +24,6 @@ export default class Portal extends React
);
- if (componentName === 'OneTap') {
- return ReactDOM.createPortal(
- {el},
- this.elRef,
- );
- }
-
if (props?.routing === 'path') {
if (!props?.path) {
clerkErrorPathRouterMissingPath(componentName);
@@ -65,3 +43,36 @@ export default class Portal extends React
return ReactDOM.createPortal({el}, node);
}
}
+
+type VirtualBodyRootPortalProps> = {
+ component: React.FunctionComponent | React.ComponentClass;
+ props?: PropsType;
+ startPath: string;
+} & Pick;
+
+export class VirtualBodyRootPortal extends React.PureComponent<
+ VirtualBodyRootPortalProps
+> {
+ private elRef = document.createElement('div');
+
+ componentDidMount() {
+ document.body.appendChild(this.elRef);
+ }
+
+ componentWillUnmount() {
+ document.body.removeChild(this.elRef);
+ }
+
+ render() {
+ const { props, startPath, component, componentName } = this.props;
+
+ return ReactDOM.createPortal(
+
+
+ {React.createElement(component, props as PortalProps['props'])}
+
+ ,
+ this.elRef,
+ );
+ }
+}
diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts
index fa9ee82c3a4..ce03e28c098 100644
--- a/packages/clerk-js/src/ui/types.ts
+++ b/packages/clerk-js/src/ui/types.ts
@@ -1,6 +1,6 @@
import type {
CreateOrganizationProps,
- OneTapProps,
+ GoogleOneTapProps,
OrganizationListProps,
OrganizationProfileProps,
OrganizationSwitcherProps,
@@ -11,7 +11,7 @@ import type {
} from '@clerk/types';
export type {
- OneTapProps,
+ GoogleOneTapProps,
SignInProps,
SignUpProps,
UserButtonProps,
@@ -74,8 +74,8 @@ export type OrganizationListCtx = OrganizationListProps & {
mode?: ComponentMode;
};
-export type OneTapCtx = OneTapProps & {
- componentName: 'OneTap';
+export type GoogleOneTapCtx = GoogleOneTapProps & {
+ componentName: 'GoogleOneTap';
};
export type AvailableComponentCtx =
@@ -87,4 +87,4 @@ export type AvailableComponentCtx =
| CreateOrganizationCtx
| OrganizationSwitcherCtx
| OrganizationListCtx
- | OneTapCtx;
+ | GoogleOneTapCtx;
diff --git a/packages/clerk-js/src/utils/one-tap.ts b/packages/clerk-js/src/utils/one-tap.ts
index 4698b64445a..5284cdd294d 100644
--- a/packages/clerk-js/src/utils/one-tap.ts
+++ b/packages/clerk-js/src/utils/one-tap.ts
@@ -15,9 +15,13 @@ interface InitializeProps {
use_fedcm_for_prompt?: boolean;
}
+interface PromptMomentNotification {
+ getMomentType: () => 'display' | 'skipped' | 'dismissed';
+}
+
interface OneTapMethods {
initialize: (params: InitializeProps) => void;
- prompt: () => void;
+ prompt: (promptListener: (promptMomentNotification: PromptMomentNotification) => void) => void;
cancel: () => void;
}
@@ -31,7 +35,7 @@ interface Google {
declare global {
export interface Window {
- google: Google;
+ google?: Google;
}
}
@@ -44,7 +48,7 @@ async function loadGIS() {
clerkFailedToLoadThirdPartyScript('Google Identity Services');
}
}
- return window.google;
+ return window.google as Google;
}
export { loadGIS };
diff --git a/packages/nextjs/src/client-boundary/uiComponents.tsx b/packages/nextjs/src/client-boundary/uiComponents.tsx
index 160ecf9ed5d..9bcb981b3a1 100644
--- a/packages/nextjs/src/client-boundary/uiComponents.tsx
+++ b/packages/nextjs/src/client-boundary/uiComponents.tsx
@@ -17,7 +17,7 @@ export {
SignOutButton,
SignInWithMetamaskButton,
OrganizationList,
- __experimental_GoogleOneTap,
+ GoogleOneTap,
} from '@clerk/clerk-react';
export const SignIn = (props: SignInProps) => {
diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts
index 0adf5348311..66cb7664660 100644
--- a/packages/nextjs/src/index.ts
+++ b/packages/nextjs/src/index.ts
@@ -31,7 +31,7 @@ export {
SignOutButton,
SignInWithMetamaskButton,
OrganizationList,
- __experimental_GoogleOneTap,
+ GoogleOneTap,
} from './client-boundary/uiComponents';
/**
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index e5b4b140ec3..9883d3642d3 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -7,7 +7,7 @@ export {
OrganizationProfile,
CreateOrganization,
OrganizationList,
- __experimental_GoogleOneTap,
+ GoogleOneTap,
} from './uiComponents';
export {
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 01ffab53b48..a9bc9f67be0 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -1,7 +1,7 @@
import { logErrorInDevMode } from '@clerk/shared';
import type {
CreateOrganizationProps,
- OneTapProps,
+ GoogleOneTapProps,
OrganizationListProps,
OrganizationProfileProps,
OrganizationSwitcherProps,
@@ -21,6 +21,7 @@ import {
} from '../errors';
import type {
MountProps,
+ OpenProps,
OrganizationProfileLinkProps,
OrganizationProfilePageProps,
UserProfileLinkProps,
@@ -86,10 +87,23 @@ type OrganizationSwitcherPropsWithoutCustomPages = Omit {
+
+const isMountProps = (props: any): props is MountProps => {
+ return 'mount' in props;
+};
+
+const isOpenProps = (props: any): props is OpenProps => {
+ return 'open' in props;
+};
+
+class Portal extends React.PureComponent {
private portalRef = React.createRef();
- componentDidUpdate(prevProps: Readonly) {
+ componentDidUpdate(prevProps: Readonly) {
+ if (!isMountProps(prevProps) || !isMountProps(this.props)) {
+ return;
+ }
+
if (
prevProps.props.appearance !== this.props.props.appearance ||
prevProps.props?.customPages?.length !== this.props.props?.customPages?.length
@@ -100,13 +114,24 @@ class Portal extends React.PureComponent {
componentDidMount() {
if (this.portalRef.current) {
- this.props.mount(this.portalRef.current, this.props.props);
+ if (isMountProps(this.props)) {
+ this.props.mount(this.portalRef.current, this.props.props);
+ }
+
+ if (isOpenProps(this.props)) {
+ this.props.open(this.props.props);
+ }
}
}
componentWillUnmount() {
if (this.portalRef.current) {
- this.props.unmount(this.portalRef.current);
+ if (isMountProps(this.props)) {
+ this.props.unmount(this.portalRef.current);
+ }
+ if (isOpenProps(this.props)) {
+ this.props.close();
+ }
}
}
@@ -114,7 +139,8 @@ class Portal extends React.PureComponent {
return (
<>
- {this.props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
+ {isMountProps(this.props) &&
+ this.props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
>
);
}
@@ -270,13 +296,12 @@ export const OrganizationList = withClerk(({ clerk, ...props }: WithClerkProp) => {
+export const GoogleOneTap = withClerk(({ clerk, ...props }: WithClerkProp) => {
return (
);
-}, 'OneTap');
+}, 'GoogleOneTap');
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index 12e10e41af3..648ed613f4c 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -3,6 +3,7 @@ import { deprecated } from '@clerk/shared/deprecated';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import type {
ActiveSessionResource,
+ AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
BeforeEmitCallback,
Clerk,
@@ -10,13 +11,13 @@ import type {
CreateOrganizationParams,
CreateOrganizationProps,
DomainOrProxyUrl,
+ GoogleOneTapProps,
HandleEmailLinkVerificationParams,
HandleMagicLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
ListenerCallback,
LoadedClerk,
- OneTapProps,
OrganizationListProps,
OrganizationMembershipResource,
OrganizationProfileProps,
@@ -27,11 +28,13 @@ import type {
SetActiveParams,
SignInProps,
SignInRedirectOptions,
+ SignInResource,
SignOut,
SignOutCallback,
SignOutOptions,
SignUpProps,
SignUpRedirectOptions,
+ SignUpResource,
UnsubscribeCallback,
UserButtonProps,
UserProfileProps,
@@ -79,8 +82,10 @@ type IsomorphicLoadedClerk = Omit<
| 'redirectToSignIn'
| 'redirectToSignUp'
| 'handleRedirectCallback'
+ | 'handleGoogleOneTapCallback'
| 'handleUnauthenticated'
| 'authenticateWithMetamask'
+ | 'authenticateWithGoogleOneTap'
| 'createOrganization'
| 'getOrganization'
| 'mountUserButton'
@@ -91,7 +96,6 @@ type IsomorphicLoadedClerk = Omit<
| 'mountSignUp'
| 'mountSignIn'
| 'mountUserProfile'
- | '__experimental_mountGoogleOneTap'
| 'client'
| 'getOrganizationMemberships'
> & {
@@ -103,9 +107,13 @@ type IsomorphicLoadedClerk = Omit<
redirectToSignUp: (options: SignUpRedirectOptions) => void;
// TODO: Align return type and parms
handleRedirectCallback: (params: HandleOAuthCallbackParams) => void;
+ handleGoogleOneTapCallback: (signInOrUp: SignInResource | SignUpResource, params: HandleOAuthCallbackParams) => void;
handleUnauthenticated: () => void;
// TODO: Align Promise unknown
authenticateWithMetamask: (params: AuthenticateWithMetamaskParams) => Promise;
+ authenticateWithGoogleOneTap: (
+ params: AuthenticateWithGoogleOneTapParams,
+ ) => Promise;
// TODO: Align return type (maybe not possible or correct)
createOrganization: (params: CreateOrganizationParams) => Promise;
// TODO: Align return type (maybe not possible or correct)
@@ -133,7 +141,6 @@ type IsomorphicLoadedClerk = Omit<
mountOrganizationProfile: (node: HTMLDivElement, props: OrganizationProfileProps) => void;
mountCreateOrganization: (node: HTMLDivElement, props: CreateOrganizationProps) => void;
mountSignUp: (node: HTMLDivElement, props: SignUpProps) => void;
- __experimental_mountGoogleOneTap: (node: HTMLDivElement, props: OneTapProps) => void;
mountSignIn: (node: HTMLDivElement, props: SignInProps) => void;
mountUserProfile: (node: HTMLDivElement, props: UserProfileProps) => void;
client: ClientResource | undefined;
@@ -146,6 +153,7 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
private readonly options: IsomorphicClerkOptions;
private readonly Clerk: ClerkProp;
private clerkjs: BrowserClerk | HeadlessBrowserClerk | null = null;
+ private preopenOneTap?: null | GoogleOneTapProps = null;
private preopenSignIn?: null | SignInProps = null;
private preopenSignUp?: null | SignUpProps = null;
private preopenUserProfile?: null | UserProfileProps = null;
@@ -331,6 +339,15 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
+ #waitForClerkJS(): Promise {
+ return new Promise(resolve => {
+ if (this.#loaded) {
+ resolve(this.clerkjs!);
+ }
+ this.addOnLoaded(() => resolve(this.clerkjs!));
+ });
+ }
+
async loadClerkJS(): Promise {
if (this.mode !== 'browser' || this.#loaded) {
return;
@@ -450,6 +467,10 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
clerkjs.openUserProfile(this.preopenUserProfile);
}
+ if (this.preopenOneTap !== null) {
+ clerkjs.openGoogleOneTap(this.preopenOneTap);
+ }
+
if (this.preopenOrganizationProfile !== null) {
clerkjs.openOrganizationProfile(this.preopenOrganizationProfile);
}
@@ -578,6 +599,22 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
+ openGoogleOneTap = (props?: GoogleOneTapProps): void => {
+ if (this.clerkjs && this.#loaded) {
+ this.clerkjs.openGoogleOneTap(props);
+ } else {
+ this.preopenOneTap = props;
+ }
+ };
+
+ closeGoogleOneTap = (): void => {
+ if (this.clerkjs && this.#loaded) {
+ this.clerkjs.closeGoogleOneTap();
+ } else {
+ this.preopenOneTap = null;
+ }
+ };
+
openUserProfile = (props?: UserProfileProps): void => {
if (this.clerkjs && this.#loaded) {
this.clerkjs.openUserProfile(props);
@@ -658,18 +695,6 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
- __experimental_mountGoogleOneTap = (node: HTMLDivElement, props: OneTapProps): void => {
- if (this.clerkjs && this.#loaded) {
- this.clerkjs.__experimental_mountGoogleOneTap(node, props);
- }
- };
-
- __experimental_unmountGoogleOneTap = (node: HTMLDivElement): void => {
- if (this.clerkjs && this.#loaded) {
- this.clerkjs.__experimental_unmountGoogleOneTap(node);
- }
- };
-
mountSignUp = (node: HTMLDivElement, props: SignUpProps): void => {
if (this.clerkjs && this.#loaded) {
this.clerkjs.mountSignUp(node, props);
@@ -894,6 +919,26 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
+ handleGoogleOneTapCallback = (
+ signInOrUp: SignInResource | SignUpResource,
+ params: HandleOAuthCallbackParams,
+ ): void => {
+ const callback = () => this.clerkjs?.handleGoogleOneTapCallback(signInOrUp, params);
+ if (this.clerkjs && this.#loaded) {
+ void callback()?.catch(() => {
+ // This error is caused when the host app is using React18
+ // and strictMode is enabled. This useEffects runs twice because
+ // the clerk-react ui components mounts, unmounts and mounts again
+ // so the clerk-js component loses its state because of the custom
+ // unmount callback we're using.
+ // This needs to be solved by tweaking the logic in uiComponents.tsx
+ // or by making handleRedirectCallback idempotent
+ });
+ } else {
+ this.premountMethodCalls.set('handleGoogleOneTapCallback', callback);
+ }
+ };
+
handleEmailLinkVerification = async (params: HandleEmailLinkVerificationParams): Promise => {
const callback = () => this.clerkjs?.handleEmailLinkVerification(params);
if (this.clerkjs && this.#loaded) {
@@ -912,6 +957,13 @@ export default class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
+ authenticateWithGoogleOneTap = async (
+ params: AuthenticateWithGoogleOneTapParams,
+ ): Promise => {
+ const clerkjs = await this.#waitForClerkJS();
+ return clerkjs.authenticateWithGoogleOneTap(params);
+ };
+
createOrganization = async (params: CreateOrganizationParams): Promise => {
const callback = () => this.clerkjs?.createOrganization(params);
if (this.clerkjs && this.#loaded) {
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index dd9bb60028b..307b4dd29aa 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -55,6 +55,12 @@ export interface MountProps {
customPagesPortals?: any[];
}
+export interface OpenProps {
+ open: (props: any) => void;
+ close: () => void;
+ props?: any;
+}
+
export interface HeadlessBrowserClerk extends Clerk {
load: (opts?: Omit) => Promise;
updateClient: (client: ClientResource) => void;
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index e18cfa3c4b5..18ee116e405 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -18,6 +18,8 @@ import type { OrganizationResource } from './organization';
import type { OrganizationInvitationResource } from './organizationInvitation';
import type { MembershipRole, OrganizationMembershipResource } from './organizationMembership';
import type { ActiveSessionResource } from './session';
+import type { SignInResource } from './signIn';
+import type { SignUpResource } from './signUp';
import type { UserResource } from './user';
import type { DeepPartial, DeepSnakeToCamel } from './utils';
@@ -128,6 +130,18 @@ export interface Clerk {
*/
closeSignIn: () => void;
+ /**
+ * Opens the Google One Tap component.
+ * @param props Optional props that will be passed to the GoogleOneTap component.
+ */
+ openGoogleOneTap: (props?: GoogleOneTapProps) => void;
+
+ /**
+ * Opens the Google One Tap component.
+ * If the component is not already open, results in a noop.
+ */
+ closeGoogleOneTap: () => void;
+
/**
* Opens the Clerk SignUp component in a modal.
* @param props Optional props that will be passed to the SignUp component.
@@ -187,22 +201,6 @@ export interface Clerk {
*/
unmountSignIn: (targetNode: HTMLDivElement) => void;
- /**
- * Mounts a Google one tap flow component at the target element.
- * @experimental
- * @param targetNode Target node to mount the GoogleOneTap component.
- * @param oneTapProps sign in configuration parameters.
- */
- __experimental_mountGoogleOneTap: (targetNode: HTMLDivElement, oneTapProps?: OneTapProps) => void;
-
- /**
- * Unmount a Google one tap flow component from the target element.
- * If there is no component mounted at the target node, results in a noop.
- * @experimental
- * @param targetNode Target node to unmount the SignIn component from.
- */
- __experimental_unmountGoogleOneTap: (targetNode: HTMLDivElement) => void;
-
/**
* Mounts a sign up flow component at the target element.
*
@@ -422,6 +420,16 @@ export interface Clerk {
*/
redirectToHome: () => void;
+ /**
+ * Completes a Google One Tap redirection flow started by
+ * {@link Clerk.authenticateWithGoogleOneTap}
+ */
+ handleGoogleOneTapCallback: (
+ signInOrUp: SignInResource | SignUpResource,
+ params: HandleOAuthCallbackParams,
+ customNavigate?: (to: string) => Promise,
+ ) => Promise;
+
/**
* Completes an OAuth or SAML redirection flow started by
* {@link Clerk.client.signIn.authenticateWithRedirect} or {@link Clerk.client.signUp.authenticateWithRedirect}
@@ -454,6 +462,13 @@ export interface Clerk {
*/
authenticateWithMetamask: (params?: AuthenticateWithMetamaskParams) => Promise;
+ /**
+ * Authenticates user using a Google token generated from Google identity services.
+ */
+ authenticateWithGoogleOneTap: (
+ params: AuthenticateWithGoogleOneTapParams,
+ ) => Promise;
+
/**
* Creates an organization, adding the current user as admin.
*/
@@ -685,8 +700,27 @@ export type SignInProps = {
initialValues?: SignInInitialValues;
} & RedirectOptions;
-export type OneTapProps = {
+type GoogleOneTapRedirectUrlProps = RedirectOptions;
+
+export type GoogleOneTapProps = GoogleOneTapRedirectUrlProps & {
+ /**
+ * Whether to cancel the Google One Tap request if a user clicks outside the prompt.
+ * @default true
+ */
cancelOnTapOutside?: boolean;
+ /**
+ * Enables upgraded One Tap UX on ITP browsers.
+ * Turning this options off, would hide any One Tap UI in such browsers.
+ * @default true
+ */
+ 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.
+ * @default true
+ */
+ fedCmSupport?: boolean;
appearance?: SignInTheme;
};
@@ -1062,6 +1096,10 @@ export interface AuthenticateWithMetamaskParams {
unsafeMetadata?: SignUpUnsafeMetadata;
}
+export interface AuthenticateWithGoogleOneTapParams {
+ token: string;
+}
+
export interface LoadedClerk extends Clerk {
client: ClientResource;
}
diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts
index a3afaa806bd..b9288e1a584 100644
--- a/packages/types/src/signIn.ts
+++ b/packages/types/src/signIn.ts
@@ -44,11 +44,11 @@ import type {
import type { ValidatePasswordCallbacks } from './passwords';
import type { AuthenticateWithRedirectParams } from './redirects';
import type { ClerkResource } from './resource';
-import type { SignUpResource } from './signUp';
import type {
BackupCodeStrategy,
EmailCodeStrategy,
EmailLinkStrategy,
+ GoogleOneTapStrategy,
OAuthStrategy,
PasswordStrategy,
PhoneCodeStrategy,
@@ -98,13 +98,6 @@ export interface SignInResource extends ClerkResource {
*/
createMagicLinkFlow: () => CreateMagicLinkFlowReturn;
- /**
- * @experimental
- */
- __experimental_authenticateWithGoogleOneTap: (
- params: __experimental_AuthenticateWithGoogleOneTapParams,
- ) => Promise;
-
createEmailLinkFlow: () => CreateEmailLinkFlowReturn;
validatePassword: (password: string, callbacks?: ValidatePasswordCallbacks) => void;
@@ -182,6 +175,10 @@ export type SignInCreateParams = (
strategy: TicketStrategy;
ticket: string;
}
+ | {
+ strategy: GoogleOneTapStrategy;
+ token: string;
+ }
| {
strategy: PasswordStrategy;
password: string;
@@ -220,10 +217,6 @@ export interface SignInStartMagicLinkFlowParams extends StartMagicLinkFlowParams
emailAddressId: string;
}
-export type __experimental_AuthenticateWithGoogleOneTapParams = {
- token: string;
-};
-
export interface SignInStartEmailLinkFlowParams extends StartEmailLinkFlowParams {
emailAddressId: string;
}
diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts
index 024993875ca..8bc0a826b82 100644
--- a/packages/types/src/signUp.ts
+++ b/packages/types/src/signUp.ts
@@ -20,6 +20,7 @@ import type { ClerkResource } from './resource';
import type {
EmailCodeStrategy,
EmailLinkStrategy,
+ GoogleOneTapStrategy,
OAuthStrategy,
PhoneCodeStrategy,
SamlStrategy,
@@ -168,12 +169,13 @@ export type SignUpCreateParams = Partial<
externalAccountStrategy: string;
externalAccountRedirectUrl: string;
externalAccountActionCompleteRedirectUrl: string;
- strategy: OAuthStrategy | SamlStrategy | TicketStrategy;
+ strategy: OAuthStrategy | SamlStrategy | TicketStrategy | GoogleOneTapStrategy;
redirectUrl: string;
actionCompleteRedirectUrl: string;
transfer: boolean;
unsafeMetadata: SignUpUnsafeMetadata;
ticket: string;
+ token: string;
} & SnakeToCamel>
>;
diff --git a/packages/types/src/strategies.ts b/packages/types/src/strategies.ts
index f6005d60fcb..c34017a6f64 100644
--- a/packages/types/src/strategies.ts
+++ b/packages/types/src/strategies.ts
@@ -1,6 +1,7 @@
import type { OAuthProvider } from './oauth';
import type { Web3Provider } from './web3';
+export type GoogleOneTapStrategy = 'google_one_tap';
export type PasswordStrategy = 'password';
export type PhoneCodeStrategy = 'phone_code';
export type EmailCodeStrategy = 'email_code';