Skip to content

Commit

Permalink
fix(clerk-react): Missing methods and properties from IsomorphicClerk (
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef committed Nov 29, 2023
1 parent 6cef817 commit 8fac836
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 26 deletions.
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.
14 changes: 9 additions & 5 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
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 @@ -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: <UserProfile.Page /> component needs to be a direct child of `<UserProfile />` or `<UserButton />`.';
Expand Down
200 changes: 194 additions & 6 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ActiveSessionResource,
AuthenticateWithMetamaskParams,
BeforeEmitCallback,
BuildUrlWithAuthParams,
Clerk,
ClientResource,
CreateOrganizationParams,
Expand All @@ -13,10 +14,16 @@ import type {
HandleEmailLinkVerificationParams,
HandleMagicLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
ListenerCallback,
LoadedClerk,
OrganizationListProps,
OrganizationMembershipResource,
OrganizationProfileProps,
OrganizationResource,
OrganizationSwitcherProps,
RedirectOptions,
SDKMetadata,
SetActiveParams,
SignInProps,
SignInRedirectOptions,
Expand All @@ -30,7 +37,6 @@ import type {
UserProfileProps,
UserResource,
} from '@clerk/types';
import type { OrganizationProfileProps, OrganizationSwitcherProps } from '@clerk/types';

import { unsupportedNonBrowserDomainOrProxyUrlFunction } from './errors';
import type {
Expand All @@ -57,10 +63,84 @@ type MethodName<T> = {

type MethodCallback = () => Promise<unknown> | 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<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;

getOrganizationMemberships: () => Promise<OrganizationMembershipResource[] | void>;
};

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

/**
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -851,8 +854,8 @@ type LooseExtractedParams<T extends string> = `:${T}` | (string & NonNullable<un

export type OrganizationSwitcherProps = {
/**
Controls the default state of the OrganizationSwitcher
*/
* Controls the default state of the OrganizationSwitcher
*/
defaultOpen?: boolean;
/**
* By default, users can switch between organization and their personal account.
Expand Down

0 comments on commit 8fac836

Please sign in to comment.