diff --git a/.changeset/mean-poets-bow.md b/.changeset/mean-poets-bow.md new file mode 100644 index 0000000000..1c4c7d93e1 --- /dev/null +++ b/.changeset/mean-poets-bow.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/clerk-react': patch +--- + +Sync IsomorphicClerk with the clerk singleton and the LoadedClerk interface. IsomorphicClerk now extends from LoadedClerk. diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 68f4cb3eba..43a403bf7f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -146,10 +146,10 @@ export class Clerk implements ClerkInterface { version: __PKG_VERSION__, }; - public client?: ClientResource; - public session?: ActiveSessionResource | null; - public organization?: OrganizationResource | null; - public user?: UserResource | null; + public client: ClientResource | undefined; + public session: ActiveSessionResource | null | undefined; + public organization: OrganizationResource | null | undefined; + public user: UserResource | null | undefined; public __internal_country?: string | null; public telemetry?: TelemetryCollector; @@ -180,6 +180,10 @@ export class Clerk implements ClerkInterface { return Clerk.version; } + get sdkMetadata(): SDKMetadata { + return Clerk.sdkMetadata; + } + get loaded(): boolean { return this.#isReady; } diff --git a/packages/react/src/errors.ts b/packages/react/src/errors.ts index 9ab1690fe6..498e47d280 100644 --- a/packages/react/src/errors.ts +++ b/packages/react/src/errors.ts @@ -23,7 +23,7 @@ export const invalidStateError = 'Invalid state. Feel free to submit a bug or reach out to support here: https://clerk.com/support'; export const unsupportedNonBrowserDomainOrProxyUrlFunction = - 'Unsupported usage of domain or proxyUrl. The usage of domain or proxyUrl as function is not supported in non-browser environments.'; + 'Unsupported usage of isSatellite, domain or proxyUrl. The usage of isSatellite, domain or proxyUrl as function is not supported in non-browser environments.'; export const userProfilePageRenderedError = ' component needs to be a direct child of `` or ``.'; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 6ce2d3f79c..c86e580bbe 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -4,6 +4,7 @@ import type { TelemetryCollector } from '@clerk/shared/telemetry'; import type { ActiveSessionResource, AuthenticateWithMetamaskParams, + BuildUrlWithAuthParams, Clerk, ClientResource, CreateOrganizationParams, @@ -11,11 +12,15 @@ import type { DomainOrProxyUrl, HandleEmailLinkVerificationParams, HandleOAuthCallbackParams, + InstanceType, ListenerCallback, + LoadedClerk, OrganizationListProps, OrganizationProfileProps, OrganizationResource, OrganizationSwitcherProps, + RedirectOptions, + SDKMetadata, SetActiveParams, SignInProps, SignInRedirectOptions, @@ -28,6 +33,7 @@ import type { UserButtonProps, UserProfileProps, UserResource, + Without, } from '@clerk/types'; import { unsupportedNonBrowserDomainOrProxyUrlFunction } from './errors'; @@ -57,7 +63,80 @@ type MethodName = { type MethodCallback = () => Promise | unknown; -export class IsomorphicClerk { +type IsomorphicLoadedClerk = Without< + LoadedClerk, + /** + * Override ClerkJS methods in order to support premountMethodCalls + */ + | 'buildSignInUrl' + | 'buildSignUpUrl' + | 'buildUserProfileUrl' + | 'buildCreateOrganizationUrl' + | 'buildOrganizationProfileUrl' + | 'buildHomeUrl' + | 'buildUrlWithAuth' + | 'redirectWithAuth' + | 'redirectToSignIn' + | 'redirectToSignUp' + | 'handleRedirectCallback' + | 'handleUnauthenticated' + | 'authenticateWithMetamask' + | 'createOrganization' + | 'getOrganization' + | 'mountUserButton' + | 'mountOrganizationList' + | 'mountOrganizationSwitcher' + | 'mountOrganizationProfile' + | 'mountCreateOrganization' + | 'mountSignUp' + | 'mountSignIn' + | 'mountUserProfile' + | 'client' +> & { + // TODO: Align return type + redirectWithAuth: (...args: Parameters) => void; + // TODO: Align return type + redirectToSignIn: (options: SignInRedirectOptions) => void; + // TODO: Align return type + redirectToSignUp: (options: SignUpRedirectOptions) => void; + // TODO: Align return type and parms + handleRedirectCallback: (params: HandleOAuthCallbackParams) => void; + handleUnauthenticated: () => void; + // TODO: Align Promise unknown + authenticateWithMetamask: (params: AuthenticateWithMetamaskParams) => Promise; + // TODO: Align return type (maybe not possible or correct) + createOrganization: (params: CreateOrganizationParams) => Promise; + // TODO: Align return type (maybe not possible or correct) + getOrganization: (organizationId: string) => Promise; + + // TODO: Align return type + buildSignInUrl: (opts?: RedirectOptions) => string | void; + // TODO: Align return type + buildSignUpUrl: (opts?: RedirectOptions) => string | void; + // TODO: Align return type + buildUserProfileUrl: () => string | void; + // TODO: Align return type + buildCreateOrganizationUrl: () => string | void; + // TODO: Align return type + buildOrganizationProfileUrl: () => string | void; + // TODO: Align return type + buildHomeUrl: () => string | void; + // TODO: Align return type + buildUrlWithAuth: (to: string, opts?: BuildUrlWithAuthParams | undefined) => string | void; + + // TODO: Align optional props + mountUserButton: (node: HTMLDivElement, props: UserButtonProps) => void; + mountOrganizationList: (node: HTMLDivElement, props: OrganizationListProps) => void; + mountOrganizationSwitcher: (node: HTMLDivElement, props: OrganizationSwitcherProps) => void; + mountOrganizationProfile: (node: HTMLDivElement, props: OrganizationProfileProps) => void; + mountCreateOrganization: (node: HTMLDivElement, props: CreateOrganizationProps) => void; + mountSignUp: (node: HTMLDivElement, props: SignUpProps) => void; + mountSignIn: (node: HTMLDivElement, props: SignInProps) => void; + mountUserProfile: (node: HTMLDivElement, props: UserProfileProps) => void; + client: ClientResource | undefined; +}; + +export class IsomorphicClerk implements IsomorphicLoadedClerk { private readonly mode: 'browser' | 'server'; private readonly options: IsomorphicClerkOptions; private readonly Clerk: ClerkProp; @@ -144,6 +223,108 @@ export class IsomorphicClerk { void this.loadClerkJS(); } + get sdkMetadata(): SDKMetadata | undefined { + return this.clerkjs?.sdkMetadata || this.options.sdkMetadata || undefined; + } + + get instanceType(): InstanceType | undefined { + return this.clerkjs?.instanceType; + } + + get frontendApi(): string { + return this.clerkjs?.frontendApi || ''; + } + + get isStandardBrowser(): boolean { + return this.clerkjs?.isStandardBrowser || this.options.standardBrowser || false; + } + + get isSatellite(): boolean { + // This getter can run in environments where window is not available. + // In those cases we should expect and use domain as a string + if (typeof window !== 'undefined' && window.location) { + return handleValueOrFn(this.options.isSatellite, new URL(window.location.href), false); + } + if (typeof this.options.isSatellite === 'function') { + return errorThrower.throw(unsupportedNonBrowserDomainOrProxyUrlFunction); + } + return false; + } + + isReady = (): boolean => Boolean(this.clerkjs?.isReady()); + + buildSignInUrl = (opts?: RedirectOptions): string | void => { + const callback = () => this.clerkjs?.buildSignInUrl(opts) || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildSignInUrl', callback); + } + }; + + buildSignUpUrl = (opts?: RedirectOptions): string | void => { + const callback = () => this.clerkjs?.buildSignUpUrl(opts) || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildSignUpUrl', callback); + } + }; + + buildUserProfileUrl = (): string | void => { + const callback = () => this.clerkjs?.buildUserProfileUrl() || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildUserProfileUrl', callback); + } + }; + + buildCreateOrganizationUrl = (): string | void => { + const callback = () => this.clerkjs?.buildCreateOrganizationUrl() || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildCreateOrganizationUrl', callback); + } + }; + + buildOrganizationProfileUrl = (): string | void => { + const callback = () => this.clerkjs?.buildOrganizationProfileUrl() || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildOrganizationProfileUrl', callback); + } + }; + + buildHomeUrl = (): string | void => { + const callback = () => this.clerkjs?.buildHomeUrl() || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildHomeUrl', callback); + } + }; + + buildUrlWithAuth = (to: string, opts?: BuildUrlWithAuthParams | undefined): string | void => { + const callback = () => this.clerkjs?.buildUrlWithAuth(to, opts) || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildUrlWithAuth', callback); + } + }; + + handleUnauthenticated = (): void => { + const callback = () => this.clerkjs?.handleUnauthenticated(); + if (this.clerkjs && this.#loaded) { + void callback(); + } else { + this.premountMethodCalls.set('handleUnauthenticated', callback); + } + }; + async loadClerkJS(): Promise { if (this.mode !== 'browser' || this.#loaded) { return; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index c38968c9d0..49ccab2843 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -59,14 +59,17 @@ export interface Clerk { /** * Clerk SDK version number. */ - version?: string; + version: string | undefined; /** * If present, contains information about the SDK that the host application is using. * For example, if Clerk is loaded through `@clerk/nextjs`, this would be `{ name: '@clerk/nextjs', version: '1.0.0' }` */ - sdkMetadata?: SDKMetadata; + sdkMetadata: SDKMetadata | undefined; + /** + * If true the bootstrapping of Clerk.load() has completed successfully. + */ loaded: boolean; frontendApi: string; @@ -75,7 +78,7 @@ export interface Clerk { publishableKey: string; /** Clerk Proxy url string. */ - proxyUrl?: string; + proxyUrl: string | undefined; /** Clerk Satellite Frontend API string. */ domain: string; @@ -84,22 +87,22 @@ export interface Clerk { isSatellite: boolean; /** Clerk Instance type is defined from the Publishable key */ - instanceType?: InstanceType; + instanceType: InstanceType | undefined; /** Clerk flag for loading Clerk in a standard browser setup */ - isStandardBrowser?: boolean; + isStandardBrowser: boolean | undefined; /** Client handling most Clerk operations. */ - client?: ClientResource; + client: ClientResource | undefined; /** Active Session. */ - session?: ActiveSessionResource | null; + session: ActiveSessionResource | null | undefined; /** Active Organization */ - organization?: OrganizationResource | null; + organization: OrganizationResource | null | undefined; /** Current User. */ - user?: UserResource | null; + user: UserResource | null | undefined; /** * Signs out the current user on single-session instances, or all users on multi-session instances @@ -760,8 +763,8 @@ export type UserButtonProps = UserButtonProfileMode & { */ showName?: boolean; /** - Controls the default state of the UserButton - */ + * Controls the default state of the UserButton + */ defaultOpen?: boolean; /** * Full URL or path to navigate after sign out is complete @@ -813,7 +816,7 @@ type CreateOrganizationMode = export type OrganizationSwitcherProps = CreateOrganizationMode & OrganizationProfileMode & { /** - Controls the default state of the OrganizationSwitcher + * Controls the default state of the OrganizationSwitcher */ defaultOpen?: boolean; /**