Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(clerk-react): Missing methods and properties from IsomorphicClerk #2226

Merged
merged 7 commits into from
Nov 29, 2023
6 changes: 6 additions & 0 deletions .changeset/mean-poets-bow.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 8 additions & 4 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -180,6 +180,10 @@ export class Clerk implements ClerkInterface {
return Clerk.version;
}

get sdkMetadata(): SDKMetadata {
return Clerk.sdkMetadata;
}

get loaded(): boolean {
return this.#isReady;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'<UserProfile.Page /> component needs to be a direct child of `<UserProfile />` or `<UserButton />`.';
Expand Down
183 changes: 182 additions & 1 deletion packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import type { TelemetryCollector } from '@clerk/shared/telemetry';
import type {
ActiveSessionResource,
AuthenticateWithMetamaskParams,
BuildUrlWithAuthParams,
Clerk,
ClientResource,
CreateOrganizationParams,
CreateOrganizationProps,
DomainOrProxyUrl,
HandleEmailLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
ListenerCallback,
LoadedClerk,
OrganizationListProps,
OrganizationProfileProps,
OrganizationResource,
OrganizationSwitcherProps,
RedirectOptions,
SDKMetadata,
SetActiveParams,
SignInProps,
SignInRedirectOptions,
Expand All @@ -28,6 +33,7 @@ import type {
UserButtonProps,
UserProfileProps,
UserResource,
Without,
} from '@clerk/types';

import { unsupportedNonBrowserDomainOrProxyUrlFunction } from './errors';
Expand Down Expand Up @@ -57,7 +63,80 @@ type MethodName<T> = {

type MethodCallback = () => Promise<unknown> | 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<Clerk['redirectWithAuth']>) => 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<void>;
// TODO: Align return type (maybe not possible or correct)
createOrganization: (params: CreateOrganizationParams) => Promise<OrganizationResource | void>;
// TODO: Align return type (maybe not possible or correct)
getOrganization: (organizationId: string) => Promise<OrganizationResource | void>;

// 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;
Expand Down Expand Up @@ -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<HeadlessBrowserClerk | BrowserClerk | undefined> {
if (this.mode !== 'browser' || this.#loaded) {
return;
Expand Down
27 changes: 15 additions & 12 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
/**
Expand Down
Loading