UNPKG

@clerk/clerk-react

Version:

Clerk.dev React library

445 lines (383 loc) • 12 kB
import { inBrowser, isConstructor, loadScript } from './utils'; import { noFrontendApiError } from './errors'; import type { BrowserClerk, BrowserClerkConstructor, ClerkProp, IsomorphicClerkOptions, } from './types'; import type { ActiveSessionResource, ClientResource, HandleMagicLinkVerificationParams, HandleOAuthCallbackParams, RedirectOptions, Resources, SessionResource, SignInProps, SignOutCallback, SignUpProps, UserButtonProps, UserProfileProps, UserResource, } from '@clerk/types'; export interface Global { Clerk?: BrowserClerk; } declare const global: Global; type MethodName<T> = { [P in keyof T]: T[P] extends Function ? P : never; }[keyof T]; type MethodCallback = () => void; export default class IsomorphicClerk { private mode: string; private frontendApi: string; private options: IsomorphicClerkOptions; private Clerk: ClerkProp; private clerkjs: BrowserClerk | null = null; private preopenSignIn?: null | SignInProps = null; private preopenSignUp?: null | SignUpProps = null; private premountSignInNodes = new Map<HTMLDivElement, SignInProps>(); private premountSignUpNodes = new Map<HTMLDivElement, SignUpProps>(); private premountUserProfileNodes = new Map< HTMLDivElement, UserProfileProps >(); private premountUserButtonNodes = new Map<HTMLDivElement, UserButtonProps>(); private premountMethodCalls = new Map< MethodName<BrowserClerk>, MethodCallback >(); private _loaded = false; ssrData: string | null = null; ssrClient?: ClientResource; ssrSession?: SessionResource | null; constructor( frontendApi: string, options: IsomorphicClerkOptions = {}, Clerk: ClerkProp = null, ) { this.frontendApi = frontendApi; this.options = options; this.Clerk = Clerk; this.mode = inBrowser() ? 'browser' : 'server'; // TODO: Support SRR for NextJS // const ssrDataNode = document.querySelector(`script[data-clerk="SSR"]`); // if (ssrDataNode) { // this.ssrData = ssrDataNode.innerHTML; // const parsedData = JSON.parse(this.ssrData); // this.ssrClient = parsedData.client; // this.ssrSession = parsedData.session; // } } async loadClerkJS(): Promise<BrowserClerk | undefined> { if (!this.frontendApi) { this.throwError(noFrontendApiError); } try { if (this.Clerk) { // Set a fixed Clerk version let c; if (isConstructor<BrowserClerkConstructor>(this.Clerk)) { // Construct a new Clerk object if a constructor is passed c = new this.Clerk(this.frontendApi); await c.load(this.options); } else { // Otherwise use the instantiated Clerk object c = this.Clerk; if (!c.isReady()) { await c.load(this.options); } } global.Clerk = c; } else { // Hot-load latest ClerkJS from Clerk CDN await loadScript(this.frontendApi, this.options.scriptUrl); if (!global.Clerk) { throw new Error( 'Failed to download latest ClerkJS. Contact support@clerk.dev.', ); } await global.Clerk.load(this.options); } return this.hydrateClerkJS(global.Clerk); } catch (err) { let message; if (err instanceof Error) { message = err.message; } else { message = String(err); } this.throwError(message); return; } } // Custom wrapper to throw an error, since we need to apply different handling between // production and development builds. In Next.js we can throw a full screen error in // development mode. However, in production throwing an error results in an infinite loop // as shown at https://github.com/vercel/next.js/issues/6973 throwError(errorMsg: string): void { if (process.env.NODE_ENV === 'production') { console.error(errorMsg); } throw new Error(errorMsg); } private hydrateClerkJS = async (clerkjs: BrowserClerk | undefined) => { if (!clerkjs) { throw new Error('Failed to hydrate latest Clerk JS'); } this.clerkjs = clerkjs; this.premountMethodCalls.forEach(cb => cb()); if (this.preopenSignIn !== null) { clerkjs.openSignIn(this.preopenSignIn); } if (this.preopenSignUp !== null) { clerkjs.openSignUp(this.preopenSignUp); } this.premountSignInNodes.forEach( (props: SignInProps, node: HTMLDivElement) => { clerkjs.mountSignIn(node, props); }, ); this.premountSignUpNodes.forEach( (props: SignUpProps, node: HTMLDivElement) => { clerkjs.mountSignUp(node, props); }, ); this.premountUserProfileNodes.forEach( (props: UserProfileProps, node: HTMLDivElement) => { clerkjs.mountUserProfile(node, props); }, ); this.premountUserButtonNodes.forEach( (props: UserButtonProps, node: HTMLDivElement) => { clerkjs.mountUserButton(node, props); }, ); this._loaded = true; return this.clerkjs; }; get version(): string | undefined { return this.clerkjs?.version; } get client(): ClientResource | undefined { if (this.clerkjs) { return this.clerkjs.client; // TODO: add ssr condition } else { return undefined; } } get session(): ActiveSessionResource | undefined | null { if (this.clerkjs) { return this.clerkjs.session; // TODO: add ssr condition } else { return undefined; } } get user(): UserResource | undefined | null { if (this.clerkjs) { return this.clerkjs.user; // TODO: add ssr condition } else { return undefined; } } // TODO: Remove temp use of __unstable__environment get __unstable__environment(): any { if (this.clerkjs) { return (this.clerkjs as any).__unstable__environment; // TODO: add ssr condition } else { return undefined; } } setSession = ( session: ActiveSessionResource | string | null, beforeEmit?: (session: ActiveSessionResource | null) => void | Promise<any>, ): Promise<void> => { if (this.clerkjs) { return this.clerkjs.setSession(session, beforeEmit); } else { return Promise.reject(); } }; openSignIn = (props?: SignInProps): void => { if (this.clerkjs && this._loaded) { this.clerkjs.openSignIn(props); } else { this.preopenSignIn = props; } }; closeSignIn = (): void => { if (this.clerkjs && this._loaded) { this.clerkjs.closeSignIn(); } else { this.preopenSignIn = null; } }; openSignUp = (props?: SignUpProps): void => { if (this.clerkjs && this._loaded) { this.clerkjs.openSignUp(props); } else { this.preopenSignUp = props; } }; closeSignUp = (): void => { if (this.clerkjs && this._loaded) { this.clerkjs.closeSignUp(); } else { this.preopenSignUp = null; } }; mountSignIn = (node: HTMLDivElement, props: SignInProps): void => { if (this.clerkjs && this._loaded) { this.clerkjs.mountSignIn(node, props); } else { this.premountSignInNodes.set(node, props); } }; unmountSignIn = (node: HTMLDivElement): void => { if (this.clerkjs && this._loaded) { this.clerkjs.unmountSignIn(node); } else { this.premountSignInNodes.delete(node); } }; mountSignUp = (node: HTMLDivElement, props: SignUpProps): void => { if (this.clerkjs && this._loaded) { this.clerkjs.mountSignUp(node, props); } else { this.premountSignUpNodes.set(node, props); } }; unmountSignUp = (node: HTMLDivElement): void => { if (this.clerkjs && this._loaded) { this.clerkjs.unmountSignUp(node); } else { this.premountSignUpNodes.delete(node); } }; mountUserProfile = (node: HTMLDivElement, props: UserProfileProps): void => { if (this.clerkjs && this._loaded) { this.clerkjs.mountUserProfile(node, props); } else { this.premountUserProfileNodes.set(node, props); } }; unmountUserProfile = (node: HTMLDivElement): void => { if (this.clerkjs && this._loaded) { this.clerkjs.unmountUserProfile(node); } else { this.premountUserProfileNodes.delete(node); } }; mountUserButton = ( node: HTMLDivElement, userButtonProps: UserButtonProps, ): void => { if (this.clerkjs && this._loaded) { this.clerkjs.mountUserButton(node, userButtonProps); } else { this.premountUserButtonNodes.set(node, userButtonProps); } }; unmountUserButton = (node: HTMLDivElement): void => { if (this.clerkjs && this._loaded) { this.clerkjs.unmountUserButton(node); } else { this.premountUserButtonNodes.delete(node); } }; addListener = (listener: (emission: Resources) => void): void => { const callback = () => this.clerkjs?.addListener(listener); if (this.clerkjs) { callback(); } else { this.premountMethodCalls.set('addListener', callback); } }; loadFromServer = (token: string): void => { if (this.mode === 'browser') { void this.throwError( 'loadFromServer cannot be called in a browser context.', ); } this.ssrData = JSON.stringify({ client: this.client, session: this.session, token: token, }); }; navigate = (to: string): void => { const callback = () => this.clerkjs?.navigate(to); if (this.clerkjs && this._loaded) { void callback(); } else { this.premountMethodCalls.set('navigate', callback); } }; // DX: deprecated <=2.4.2 // Deprecate the boolean type before removing returnBack redirectToSignIn = (opts: RedirectOptions | boolean): void => { const callback = () => this.clerkjs?.redirectToSignIn(opts as any); if (this.clerkjs && this._loaded) { void callback(); } else { this.premountMethodCalls.set('redirectToSignIn', callback); } }; // DX: deprecated <=2.4.2 // Deprecate the boolean type before removing returnBack redirectToSignUp = (opts: RedirectOptions | boolean): void => { const callback = () => this.clerkjs?.redirectToSignUp(opts as any); if (this.clerkjs && this._loaded) { void callback(); } else { this.premountMethodCalls.set('redirectToSignUp', callback); } }; redirectToUserProfile = (): void => { const callback = () => this.clerkjs?.redirectToUserProfile(); if (this.clerkjs && this._loaded) { callback(); } else { this.premountMethodCalls.set('redirectToUserProfile', callback); } }; handleRedirectCallback = (params: HandleOAuthCallbackParams): void => { const callback = () => this.clerkjs?.handleRedirectCallback(params); if (this.clerkjs && this._loaded) { void callback(); } else { this.premountMethodCalls.set('handleRedirectCallback', callback); } }; handleMagicLinkVerification = async ( params: HandleMagicLinkVerificationParams, ): Promise<void> => { const callback = () => this.clerkjs?.handleMagicLinkVerification(params); if (this.clerkjs && this._loaded) { return callback() as Promise<void>; } else { this.premountMethodCalls.set('handleMagicLinkVerification', callback); } }; signOut = async (signOutCallback?: SignOutCallback): Promise<void> => { const callback = () => this.clerkjs?.signOut(signOutCallback); if (this.clerkjs && this._loaded) { return callback() as Promise<void>; } else { this.premountMethodCalls.set('signOut', callback); } }; signOutOne = async (signOutCallback?: SignOutCallback): Promise<void> => { const callback = () => this.clerkjs?.signOutOne(signOutCallback); if (this.clerkjs && this._loaded) { return callback() as Promise<void>; } else { this.premountMethodCalls.set('signOutOne', callback); } }; }