From 7130db0fbe7a6bc0c8a904d3af9f79e7e0d84c8b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 29 Nov 2023 20:31:25 +0200 Subject: [PATCH] fix(clerk-react): Missing methods and properties from IsomorphicClerk (#2226) (#2233) --- .changeset/mean-poets-bow.md | 6 + packages/clerk-js/src/core/clerk.ts | 14 +- packages/react/src/errors.ts | 2 +- packages/react/src/isomorphicClerk.ts | 200 +++++++++++++++++++++++++- packages/types/src/clerk.ts | 31 ++-- 5 files changed, 227 insertions(+), 26 deletions(-) create mode 100644 .changeset/mean-poets-bow.md 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 f085cd0ba9..59e5b219f8 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -154,13 +154,13 @@ export default 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 readonly frontendApi: string; - public readonly publishableKey?: string; + public readonly publishableKey: string | undefined; protected internal_last_error: ClerkAPIError | null = null; @@ -191,6 +191,10 @@ export default 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 0e881c3de3..521817ecd7 100644 --- a/packages/react/src/errors.ts +++ b/packages/react/src/errors.ts @@ -30,7 +30,7 @@ export const invalidStateError = 'Clerk: Invalid state. Feel free to submit a bug or reach out to support here: https://clerk.com/support'; export const unsupportedNonBrowserDomainOrProxyUrlFunction = - 'Clerk: Unsupported usage of domain or proxyUrl. The usage of domain or proxyUrl as function is not supported in non-browser environments.'; + 'Clerk: 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 = 'Clerk: component needs to be a direct child of `` or ``.'; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 9a30c028fe..bdfd174830 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -5,6 +5,7 @@ import type { ActiveSessionResource, AuthenticateWithMetamaskParams, BeforeEmitCallback, + BuildUrlWithAuthParams, Clerk, ClientResource, CreateOrganizationParams, @@ -13,10 +14,16 @@ import type { HandleEmailLinkVerificationParams, HandleMagicLinkVerificationParams, HandleOAuthCallbackParams, + InstanceType, ListenerCallback, + LoadedClerk, OrganizationListProps, OrganizationMembershipResource, + OrganizationProfileProps, OrganizationResource, + OrganizationSwitcherProps, + RedirectOptions, + SDKMetadata, SetActiveParams, SignInProps, SignInRedirectOptions, @@ -30,7 +37,6 @@ import type { UserProfileProps, UserResource, } from '@clerk/types'; -import type { OrganizationProfileProps, OrganizationSwitcherProps } from '@clerk/types'; import { unsupportedNonBrowserDomainOrProxyUrlFunction } from './errors'; import type { @@ -57,10 +63,84 @@ type MethodName = { type MethodCallback = () => Promise | unknown; -export default class IsomorphicClerk { +type IsomorphicLoadedClerk = Omit< + 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' + | 'getOrganizationMemberships' +> & { + // 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; + + getOrganizationMemberships: () => Promise; +}; + +export default class IsomorphicClerk implements IsomorphicLoadedClerk { private readonly mode: 'browser' | 'server'; - private readonly frontendApi?: string; - private readonly publishableKey?: string; private readonly options: IsomorphicClerkOptions; private readonly Clerk: ClerkProp; private clerkjs: BrowserClerk | HeadlessBrowserClerk | null = null; @@ -83,6 +163,12 @@ export default class IsomorphicClerk { #loaded = false; #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; + #frontendApi: string | undefined; + #publishableKey: string | undefined; + + get publishableKey(): string | undefined { + return this.#publishableKey; + } get loaded(): boolean { return this.#loaded; @@ -127,8 +213,8 @@ export default class IsomorphicClerk { constructor(options: IsomorphicClerkOptions) { const { Clerk = null, frontendApi, publishableKey } = options || {}; - this.frontendApi = frontendApi; - this.publishableKey = publishableKey; + this.#frontendApi = frontendApi; + this.#publishableKey = publishableKey; this.#proxyUrl = options?.proxyUrl; this.#domain = options?.domain; this.options = options; @@ -137,6 +223,108 @@ export default 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 || this.#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') { + throw new Error(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 044d1b936a..6f9ed2c406 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -60,14 +60,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; /** @@ -77,10 +80,10 @@ export interface Clerk { frontendApi: string; /** Clerk Publishable Key string. */ - publishableKey?: string; + publishableKey: string | undefined; /** Clerk Proxy url string. */ - proxyUrl?: string; + proxyUrl: string | undefined; /** Clerk Satellite Frontend API string. */ domain: string; @@ -89,22 +92,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 @@ -790,8 +793,8 @@ export type UserButtonProps = { */ 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 @@ -851,8 +854,8 @@ type LooseExtractedParams = `:${T}` | (string & NonNullable