Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/warm-pumas-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

Replace mount with open for GoogleOneTap. New api is `__experimental_openGoogleOneTap`.
36 changes: 12 additions & 24 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,18 @@ export class Clerk implements ClerkInterface {
}
};

public __experimental_openGoogleOneTap = (props?: OneTapProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls
.ensureMounted({ preloadHint: 'OneTap' })
.then(controls => controls.openModal('googleOneTap', props || {}));
};

public __experimental_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)) {
Expand Down Expand Up @@ -460,30 +472,6 @@ export 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(eventPrebuiltComponentMounted('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 =>
Expand Down
33 changes: 29 additions & 4 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ClerkOptions,
CreateOrganizationProps,
EnvironmentResource,
OneTapProps,
OrganizationProfileProps,
SignInProps,
SignUpProps,
Expand All @@ -32,6 +33,7 @@ import {
LazyComponentRenderer,
LazyImpersonationFabProvider,
LazyModalRenderer,
LazyOneTapRenderer,
LazyProviders,
} from './lazyModules/providers';
import type { AvailableComponentProps } from './types';
Expand All @@ -52,11 +54,15 @@ export type ComponentControls = {
node?: HTMLDivElement;
props?: unknown;
}) => void;
openModal: <T extends 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization'>(
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;
};
Expand All @@ -78,6 +84,7 @@ interface ComponentsProps {
interface ComponentsState {
appearance: Appearance | undefined;
options: ClerkOptions | undefined;
googleOneTapModal: null | OneTapProps;
signInModal: null | SignInProps;
signUpModal: null | SignUpProps;
userProfileModal: null | UserProfileProps;
Expand Down Expand Up @@ -153,6 +160,7 @@ const Components = (props: ComponentsProps) => {
const [state, setState] = React.useState<ComponentsState>({
appearance: props.options.appearance,
options: props.options,
googleOneTapModal: null,
signInModal: null,
signUpModal: null,
userProfileModal: null,
Expand All @@ -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();

Expand Down Expand Up @@ -219,6 +234,15 @@ const Components = (props: ComponentsProps) => {
props.onComponentsMounted();
}, []);

const mountedOneTapModal = (
<LazyOneTapRenderer
componentProps={googleOneTapModal}
globalAppearance={state.appearance}
componentAppearance={googleOneTapModal?.appearance}
startPath={buildVirtualRouterUrl({ base: '/one-tap', path: '' })}
/>
);

const mountedSignInModal = (
<LazyModalRenderer
globalAppearance={state.appearance}
Expand Down Expand Up @@ -323,6 +347,7 @@ const Components = (props: ComponentsProps) => {
);
})}

{googleOneTapModal && mountedOneTapModal}
{signInModal && mountedSignInModal}
{signUpModal && mountedSignUpModal}
{userProfileModal && mountedUserProfileModal}
Expand Down
50 changes: 29 additions & 21 deletions packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,41 +53,49 @@ function _OneTapStart(): JSX.Element | null {
const environmentClientID = environment.displayConfig.googleOneTapClientId;
const shouldLoadGIS = !user?.id && !!environmentClientID;

async function initializeGIS() {
const google = await loadGIS();
google.accounts.id.initialize({
client_id: environmentClientID!,
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
*/
useFetch(shouldLoadGIS ? loadGIS : undefined, 'google-identity-services-script', {
onSuccess(google) {
google.accounts.id.initialize({
client_id: environmentClientID!,
callback: oneTapCallback,
itp_support: ctx.itpSupport,
cancel_on_tap_outside: ctx.cancelOnTapOutside,
auto_select: false,
use_fedcm_for_prompt: ctx.fedCmSupport,
});

google.accounts.id.prompt();
isPromptedRef.current = true;
},
});
const { data: initializedGoogle } = useFetch(
shouldLoadGIS ? initializeGIS : undefined,
'google-identity-services-script',
);

useEffect(() => {
if (window.google && !user?.id && !isPromptedRef.current) {
window.google.accounts.id.prompt();
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.__experimental_closeGoogleOneTap();
}
});
isPromptedRef.current = true;
}
}, [user?.id]);
}, [clerk, initializedGoogle, user?.id]);

// Trigger only on mount/unmount. Above we handle the logic for the initial fetch + initialization
useEffect(() => {
return () => {
if (window.google && isPromptedRef.current) {
if (initializedGoogle && isPromptedRef.current) {
isPromptedRef.current = false;
window.google.accounts.id.cancel();
initializedGoogle.accounts.id.cancel();
}
};
}, []);
}, [initializedGoogle]);

return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,8 @@ export const useGoogleOneTapContext = () => {
options,
{
...ctx,
redirectUrl: window.location.href,
signInFallbackRedirectUrl: window.location.href,
signUpFallbackRedirectUrl: window.location.href,
Comment thread
panteliselef marked this conversation as resolved.
},
queryParams,
);
Expand Down
25 changes: 25 additions & 0 deletions packages/clerk-js/src/ui/lazyModules/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Appearance } from '@clerk/types';
import React, { lazy, Suspense } from 'react';

import type { FlowMetadata } from '../elements';
import { VirtualBodyRootPortal } from '../portal';
import type { ThemableCssProp } from '../styledSystem';
import type { ClerkComponentName } from './components';
import { ClerkComponents } from './components';
Expand Down Expand Up @@ -128,3 +129,27 @@ export const LazyImpersonationFabProvider = (
</Suspense>
);
};

type LazyOneTapRendererProps = React.PropsWithChildren<
{
componentProps: any;
startPath: string;
} & Omit<AppearanceProviderProps, 'appearanceKey'>
>;

export const LazyOneTapRenderer = (props: LazyOneTapRendererProps) => {
Comment thread
octoper marked this conversation as resolved.
return (
<AppearanceProvider
globalAppearance={props.globalAppearance}
appearanceKey={'oneTap'}
appearance={props.componentAppearance}
>
<VirtualBodyRootPortal
startPath={props.startPath}
component={ClerkComponents['OneTap']}
props={props.componentProps}
componentName={'OneTap'}
/>
</AppearanceProvider>
);
};
53 changes: 33 additions & 20 deletions packages/clerk-js/src/ui/portal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import ReactDOM from 'react-dom';

import { PRESERVED_QUERYSTRING_PARAMS } from '../../core/constants';
import { clerkErrorPathRouterMissingPath } from '../../core/errors';
import { buildVirtualRouterUrl } from '../../utils';
import { normalizeRoutingOptions } from '../../utils/normalizeRoutingOptions';
import { ComponentContext } from '../contexts';
import { HashRouter, PathRouter, VirtualRouter } from '../router';
Expand All @@ -18,18 +17,6 @@ type PortalProps<CtxType extends AvailableComponentCtx, PropsType = Omit<CtxType
} & Pick<CtxType, 'componentName'>;

export class Portal<CtxType extends AvailableComponentCtx> extends React.PureComponent<PortalProps<CtxType>> {
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);
}
}
render() {
const { props, component, componentName, node } = this.props;
const normalizedProps = { ...props, ...normalizeRoutingOptions({ routing: props?.routing, path: props?.path }) };
Expand All @@ -42,13 +29,6 @@ export class Portal<CtxType extends AvailableComponentCtx> extends React.PureCom
</ComponentContext.Provider>
);

if (componentName === 'OneTap') {
return ReactDOM.createPortal(
<VirtualRouter startPath={buildVirtualRouterUrl({ base: '/one-tap', path: '' })}>{el}</VirtualRouter>,
this.elRef,
);
}

if (normalizedProps?.routing === 'path') {
if (!normalizedProps?.path) {
clerkErrorPathRouterMissingPath(componentName);
Expand All @@ -68,3 +48,36 @@ export class Portal<CtxType extends AvailableComponentCtx> extends React.PureCom
return ReactDOM.createPortal(<HashRouter preservedParams={PRESERVED_QUERYSTRING_PARAMS}>{el}</HashRouter>, node);
}
}

type VirtualBodyRootPortalProps<CtxType extends AvailableComponentCtx, PropsType = Omit<CtxType, 'componentName'>> = {
component: React.FunctionComponent<PropsType> | React.ComponentClass<PropsType, any>;
props?: PropsType;
startPath: string;
} & Pick<CtxType, 'componentName'>;

export class VirtualBodyRootPortal<CtxType extends AvailableComponentCtx> extends React.PureComponent<
VirtualBodyRootPortalProps<CtxType>
> {
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(
<VirtualRouter startPath={startPath}>
<ComponentContext.Provider value={{ componentName: componentName, ...props } as CtxType}>
<Suspense fallback={''}>{React.createElement(component, props as PortalProps<CtxType>['props'])}</Suspense>
</ComponentContext.Provider>
</VirtualRouter>,
this.elRef,
);
}
}
6 changes: 5 additions & 1 deletion packages/clerk-js/src/utils/one-tap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading