From 7faf5d2026ea710616057b444dfa44e10be939a2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 5 Mar 2025 10:00:27 -0600 Subject: [PATCH 01/31] feat(clerk-js): allow initialization with defaults --- .../clerk-js/src/core/resources/AuthConfig.ts | 18 +- packages/clerk-js/src/core/resources/Base.ts | 9 +- .../src/core/resources/DisplayConfig.ts | 197 +++++++++--------- .../src/core/resources/Environment.ts | 30 +-- .../core/resources/OrganizationSettings.ts | 46 ++-- .../src/core/resources/UserSettings.ts | 95 ++++----- 6 files changed, 208 insertions(+), 187 deletions(-) diff --git a/packages/clerk-js/src/core/resources/AuthConfig.ts b/packages/clerk-js/src/core/resources/AuthConfig.ts index dae725e7db4..8f5d34f1c82 100644 --- a/packages/clerk-js/src/core/resources/AuthConfig.ts +++ b/packages/clerk-js/src/core/resources/AuthConfig.ts @@ -4,24 +4,30 @@ import { unixEpochToDate } from '../../utils/date'; import { BaseResource } from './internal'; export class AuthConfig extends BaseResource implements AuthConfigResource { - singleSessionMode!: boolean; claimedAt: Date | null = null; + singleSessionMode: boolean = true; - public constructor(data: AuthConfigJSON) { + public constructor(data: AuthConfigJSON | null = null) { super(); - this.fromJSON(data); + if (data) { + this.fromJSON(data); + } } protected fromJSON(data: AuthConfigJSON | null): this { - this.singleSessionMode = data ? data.single_session_mode : true; - this.claimedAt = data?.claimed_at ? unixEpochToDate(data.claimed_at) : null; + if (!data) { + return this; + } + this.claimedAt = data.claimed_at ? unixEpochToDate(data.claimed_at) : null; + this.singleSessionMode = data.single_session_mode ?? true; + return this; } public __internal_toSnapshot(): AuthConfigJSONSnapshot { return { object: 'auth_config', - id: this.id || '', + id: this.id ?? '', single_session_mode: this.singleSessionMode, claimed_at: this.claimedAt ? this.claimedAt.getTime() : null, }; diff --git a/packages/clerk-js/src/core/resources/Base.ts b/packages/clerk-js/src/core/resources/Base.ts index 105e255a37c..ecc0fe0d6f1 100644 --- a/packages/clerk-js/src/core/resources/Base.ts +++ b/packages/clerk-js/src/core/resources/Base.ts @@ -77,7 +77,7 @@ export abstract class BaseResource { clerkMissingFapiClientInResources(); } - let fapiResponse: FapiResponse; + let fapiResponse: FapiResponse | undefined; const { fetchMaxTries } = opts; try { @@ -92,10 +92,15 @@ export abstract class BaseResource { console.warn(e); return null; } else { - throw e; + console.error(e); } } + if (typeof fapiResponse === 'undefined') { + // handle offline case + return null; + } + const { payload, status, statusText, headers } = fapiResponse; if (headers) { diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index c9fb68c4a89..bddd3225458 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -12,49 +12,51 @@ import type { import { BaseResource } from './internal'; export class DisplayConfig extends BaseResource implements DisplayConfigResource { - id!: string; - afterSignInUrl!: string; - afterSignOutAllUrl!: string; - afterSignOutOneUrl!: string; - afterSignOutUrl!: string; - afterSignUpUrl!: string; - afterSwitchSessionUrl!: string; - applicationName!: string; - backendHost!: string; - branded!: boolean; - captchaPublicKey: string | null = null; - captchaWidgetType: CaptchaWidgetType = null; + afterCreateOrganizationUrl: string = ''; + afterJoinWaitlistUrl: string = ''; + afterLeaveOrganizationUrl: string = ''; + afterSignInUrl: string = ''; + afterSignOutAllUrl: string = ''; + afterSignOutOneUrl: string = ''; + afterSignOutUrl: string = ''; + afterSignUpUrl: string = ''; + afterSwitchSessionUrl: string = ''; + applicationName: string = ''; + backendHost: string = ''; + branded: boolean = false; + captchaHeartbeat: boolean = false; + captchaHeartbeatIntervalMs?: number; + captchaOauthBypass: OAuthStrategy[] = ['oauth_google', 'oauth_microsoft', 'oauth_apple']; captchaProvider: CaptchaProvider = 'turnstile'; + captchaPublicKey: string | null = null; captchaPublicKeyInvisible: string | null = null; - captchaOauthBypass: OAuthStrategy[] = []; - captchaHeartbeat: boolean = false; - captchaHeartbeatIntervalMs?: number = undefined; - homeUrl!: string; - instanceEnvironmentType!: string; - faviconImageUrl!: string; - logoImageUrl!: string; - preferredSignInStrategy!: PreferredSignInStrategy; - signInUrl!: string; - signUpUrl!: string; - supportEmail!: string; - theme!: DisplayThemeJSON; - userProfileUrl!: string; + captchaWidgetType: CaptchaWidgetType = null; clerkJSVersion?: string; + createOrganizationUrl: string = ''; experimental__forceOauthFirst?: boolean; - organizationProfileUrl!: string; - createOrganizationUrl!: string; - afterLeaveOrganizationUrl!: string; - afterCreateOrganizationUrl!: string; + faviconImageUrl: string = ''; googleOneTapClientId?: string; - showDevModeWarning!: boolean; - termsUrl!: string; - privacyPolicyUrl!: string; - waitlistUrl!: string; - afterJoinWaitlistUrl!: string; + homeUrl: string = ''; + id: string = ''; + instanceEnvironmentType: string = ''; + logoImageUrl: string = ''; + organizationProfileUrl: string = ''; + preferredSignInStrategy: PreferredSignInStrategy = 'password'; + privacyPolicyUrl: string = ''; + showDevModeWarning: boolean = false; + signInUrl: string = ''; + signUpUrl: string = ''; + supportEmail: string = ''; + termsUrl: string = ''; + theme: DisplayThemeJSON = {} as DisplayThemeJSON; + userProfileUrl: string = ''; + waitlistUrl: string = ''; - public constructor(data: DisplayConfigJSON | DisplayConfigJSONSnapshot) { + public constructor(data?: DisplayConfigJSON | DisplayConfigJSONSnapshot | null) { super(); - this.fromJSON(data); + if (data) { + this.fromJSON(data); + } } protected fromJSON(data: DisplayConfigJSON | DisplayConfigJSONSnapshot | null): this { @@ -62,86 +64,85 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource return this; } - this.id = data.id; - this.instanceEnvironmentType = data.instance_environment_type; - this.applicationName = data.application_name; - this.theme = data.theme; - this.preferredSignInStrategy = data.preferred_sign_in_strategy; - this.logoImageUrl = data.logo_image_url; - this.faviconImageUrl = data.favicon_image_url; - this.homeUrl = data.home_url; - this.signInUrl = data.sign_in_url; - this.signUpUrl = data.sign_up_url; - this.userProfileUrl = data.user_profile_url; - this.afterSignInUrl = data.after_sign_in_url; - this.afterSignUpUrl = data.after_sign_up_url; - this.afterSignOutOneUrl = data.after_sign_out_one_url; - this.afterSignOutAllUrl = data.after_sign_out_all_url; - this.afterSwitchSessionUrl = data.after_switch_session_url; - this.branded = data.branded; - this.captchaPublicKey = data.captcha_public_key; - this.captchaWidgetType = data.captcha_widget_type; - this.captchaProvider = data.captcha_provider; - this.captchaPublicKeyInvisible = data.captcha_public_key_invisible; - // These are the OAuth strategies we used to bypass the captcha for by default - // before the introduction of the captcha_oauth_bypass field - this.captchaOauthBypass = data.captcha_oauth_bypass || ['oauth_google', 'oauth_microsoft', 'oauth_apple']; - this.captchaHeartbeat = data.captcha_heartbeat || false; - this.captchaHeartbeatIntervalMs = data.captcha_heartbeat_interval_ms; - this.supportEmail = data.support_email || ''; - this.clerkJSVersion = data.clerk_js_version; - this.organizationProfileUrl = data.organization_profile_url; - this.createOrganizationUrl = data.create_organization_url; - this.afterLeaveOrganizationUrl = data.after_leave_organization_url; - this.afterCreateOrganizationUrl = data.after_create_organization_url; - this.googleOneTapClientId = data.google_one_tap_client_id; - this.showDevModeWarning = data.show_devmode_warning; - this.termsUrl = data.terms_url; - this.privacyPolicyUrl = data.privacy_policy_url; - this.waitlistUrl = data.waitlist_url; - this.afterJoinWaitlistUrl = data.after_join_waitlist_url; + this.afterCreateOrganizationUrl = data.after_create_organization_url || this.afterCreateOrganizationUrl; + this.afterJoinWaitlistUrl = data.after_join_waitlist_url || this.afterJoinWaitlistUrl; + this.afterLeaveOrganizationUrl = data.after_leave_organization_url || this.afterLeaveOrganizationUrl; + this.afterSignInUrl = data.after_sign_in_url || this.afterSignInUrl; + this.afterSignOutAllUrl = data.after_sign_out_all_url || this.afterSignOutAllUrl; + this.afterSignOutOneUrl = data.after_sign_out_one_url || this.afterSignOutOneUrl; + this.afterSignUpUrl = data.after_sign_up_url || this.afterSignUpUrl; + this.afterSwitchSessionUrl = data.after_switch_session_url || this.afterSwitchSessionUrl; + this.applicationName = data.application_name || this.applicationName; + this.branded = data.branded ?? this.branded; + this.captchaHeartbeat = data.captcha_heartbeat ?? this.captchaHeartbeat; + this.captchaHeartbeatIntervalMs = data.captcha_heartbeat_interval_ms ?? this.captchaHeartbeatIntervalMs; + this.captchaOauthBypass = data.captcha_oauth_bypass || this.captchaOauthBypass; + this.captchaProvider = data.captcha_provider || this.captchaProvider; + this.captchaPublicKey = data.captcha_public_key || this.captchaPublicKey; + this.captchaPublicKeyInvisible = data.captcha_public_key_invisible || this.captchaPublicKeyInvisible; + this.captchaWidgetType = data.captcha_widget_type || this.captchaWidgetType; + this.clerkJSVersion = data.clerk_js_version || this.clerkJSVersion; + this.createOrganizationUrl = data.create_organization_url || this.createOrganizationUrl; + this.faviconImageUrl = data.favicon_image_url || this.faviconImageUrl; + this.googleOneTapClientId = data.google_one_tap_client_id || this.googleOneTapClientId; + this.homeUrl = data.home_url || this.homeUrl; + this.id = data.id || this.id; + this.instanceEnvironmentType = data.instance_environment_type || this.instanceEnvironmentType; + this.logoImageUrl = data.logo_image_url || this.logoImageUrl; + this.organizationProfileUrl = data.organization_profile_url || this.organizationProfileUrl; + this.preferredSignInStrategy = data.preferred_sign_in_strategy || this.preferredSignInStrategy; + this.privacyPolicyUrl = data.privacy_policy_url || this.privacyPolicyUrl; + this.showDevModeWarning = data.show_devmode_warning ?? this.showDevModeWarning; + this.signInUrl = data.sign_in_url || this.signInUrl; + this.signUpUrl = data.sign_up_url || this.signUpUrl; + this.supportEmail = data.support_email || this.supportEmail; + this.termsUrl = data.terms_url || this.termsUrl; + this.theme = data.theme || this.theme; + this.userProfileUrl = data.user_profile_url || this.userProfileUrl; + this.waitlistUrl = data.waitlist_url || this.waitlistUrl; + return this; } public __internal_toSnapshot(): DisplayConfigJSONSnapshot { return { object: 'display_config', - id: this.id, - instance_environment_type: this.instanceEnvironmentType, - application_name: this.applicationName, - theme: this.theme, - preferred_sign_in_strategy: this.preferredSignInStrategy, - logo_image_url: this.logoImageUrl, - favicon_image_url: this.faviconImageUrl, - home_url: this.homeUrl, - sign_in_url: this.signInUrl, - sign_up_url: this.signUpUrl, - user_profile_url: this.userProfileUrl, + after_create_organization_url: this.afterCreateOrganizationUrl, + after_join_waitlist_url: this.afterJoinWaitlistUrl, + after_leave_organization_url: this.afterLeaveOrganizationUrl, after_sign_in_url: this.afterSignInUrl, - after_sign_up_url: this.afterSignUpUrl, - after_sign_out_one_url: this.afterSignOutOneUrl, after_sign_out_all_url: this.afterSignOutAllUrl, + after_sign_out_one_url: this.afterSignOutOneUrl, + after_sign_up_url: this.afterSignUpUrl, after_switch_session_url: this.afterSwitchSessionUrl, + application_name: this.applicationName, branded: this.branded, - captcha_public_key: this.captchaPublicKey, - captcha_widget_type: this.captchaWidgetType, + captcha_heartbeat_interval_ms: this.captchaHeartbeatIntervalMs, + captcha_heartbeat: this.captchaHeartbeat, + captcha_oauth_bypass: this.captchaOauthBypass, captcha_provider: this.captchaProvider, captcha_public_key_invisible: this.captchaPublicKeyInvisible, - captcha_oauth_bypass: this.captchaOauthBypass, - captcha_heartbeat: this.captchaHeartbeat, - captcha_heartbeat_interval_ms: this.captchaHeartbeatIntervalMs, - support_email: this.supportEmail, + captcha_public_key: this.captchaPublicKey, + captcha_widget_type: this.captchaWidgetType, clerk_js_version: this.clerkJSVersion, - organization_profile_url: this.organizationProfileUrl, create_organization_url: this.createOrganizationUrl, - after_leave_organization_url: this.afterLeaveOrganizationUrl, - after_create_organization_url: this.afterCreateOrganizationUrl, + favicon_image_url: this.faviconImageUrl, google_one_tap_client_id: this.googleOneTapClientId, + home_url: this.homeUrl, + id: this.id, + instance_environment_type: this.instanceEnvironmentType, + logo_image_url: this.logoImageUrl, + organization_profile_url: this.organizationProfileUrl, + preferred_sign_in_strategy: this.preferredSignInStrategy, + privacy_policy_url: this.privacyPolicyUrl, show_devmode_warning: this.showDevModeWarning, + sign_in_url: this.signInUrl, + sign_up_url: this.signUpUrl, + support_email: this.supportEmail, terms_url: this.termsUrl, - privacy_policy_url: this.privacyPolicyUrl, + theme: this.theme, + user_profile_url: this.userProfileUrl, waitlist_url: this.waitlistUrl, - after_join_waitlist_url: this.afterJoinWaitlistUrl, }; } } diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index 7a9d9b379cc..ba53a28e11c 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -15,11 +15,11 @@ export class Environment extends BaseResource implements EnvironmentResource { private static instance: Environment; pathRoot = '/environment'; - authConfig!: AuthConfigResource; - displayConfig!: DisplayConfigResource; - userSettings!: UserSettingsResource; - organizationSettings!: OrganizationSettingsResource; - maintenanceMode!: boolean; + authConfig: AuthConfigResource = new AuthConfig(); + displayConfig: DisplayConfigResource = new DisplayConfig(); + userSettings: UserSettingsResource = new UserSettings(); + organizationSettings: OrganizationSettingsResource = new OrganizationSettings(); + maintenanceMode: boolean = false; public static getInstance(): Environment { if (!Environment.instance) { @@ -31,7 +31,10 @@ export class Environment extends BaseResource implements EnvironmentResource { constructor(data: EnvironmentJSON | EnvironmentJSONSnapshot | null = null) { super(); - this.fromJSON(data); + + if (data) { + this.fromJSON(data); + } } fetch({ touch, fetchMaxTries }: { touch: boolean; fetchMaxTries?: number } = { touch: false }): Promise { @@ -58,13 +61,16 @@ export class Environment extends BaseResource implements EnvironmentResource { }; protected fromJSON(data: EnvironmentJSONSnapshot | EnvironmentJSON | null): this { - if (data) { - this.authConfig = new AuthConfig(data.auth_config); - this.displayConfig = new DisplayConfig(data.display_config); - this.userSettings = new UserSettings(data.user_settings); - this.organizationSettings = new OrganizationSettings(data.organization_settings); - this.maintenanceMode = data.maintenance_mode; + if (!data) { + return this; } + + this.authConfig = new AuthConfig(data.auth_config); + this.userSettings = new UserSettings(data.user_settings); + this.organizationSettings = new OrganizationSettings(data.organization_settings); + this.displayConfig = new DisplayConfig(data.display_config); + this.maintenanceMode = data.maintenance_mode || this.maintenanceMode; + return this; } diff --git a/packages/clerk-js/src/core/resources/OrganizationSettings.ts b/packages/clerk-js/src/core/resources/OrganizationSettings.ts index 183f8e55b45..5dfe637a0d5 100644 --- a/packages/clerk-js/src/core/resources/OrganizationSettings.ts +++ b/packages/clerk-js/src/core/resources/OrganizationSettings.ts @@ -8,32 +8,44 @@ import type { import { BaseResource } from './internal'; export class OrganizationSettings extends BaseResource implements OrganizationSettingsResource { - enabled!: boolean; - maxAllowedMemberships!: number; - actions!: { - adminDelete: boolean; - }; - domains!: { + actions: { adminDelete: boolean } = { adminDelete: false }; + domains: { enabled: boolean; enrollmentModes: OrganizationEnrollmentMode[]; defaultRole: string | null; + } = { + enabled: false, + enrollmentModes: [], + defaultRole: null, }; + enabled: boolean = false; + maxAllowedMemberships: number = 0; - public constructor(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot) { + public constructor(data?: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null) { super(); - this.fromJSON(data); + if (data) { + this.fromJSON(data); + } } protected fromJSON(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null): this { - const { enabled = false, max_allowed_memberships = 0, actions, domains } = data || {}; - this.enabled = enabled; - this.maxAllowedMemberships = max_allowed_memberships; - this.actions = { adminDelete: actions?.admin_delete || false }; - this.domains = { - enabled: domains?.enabled || false, - enrollmentModes: domains?.enrollment_modes || [], - defaultRole: domains?.default_role || null, - }; + if (!data) { + return this; + } + + if (data.actions) { + this.actions.adminDelete = data.actions.admin_delete ?? this.actions.adminDelete; + } + + if (data.domains) { + this.domains.enabled = data.domains.enabled ?? this.domains.enabled; + this.domains.enrollmentModes = data.domains.enrollment_modes ?? this.domains.enrollmentModes; + this.domains.defaultRole = data.domains.default_role ?? this.domains.defaultRole; + } + + this.enabled = data.enabled ?? this.enabled; + this.maxAllowedMemberships = data.max_allowed_memberships ?? this.maxAllowedMemberships; + return this; } diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 15f9abb9c3e..530111e0fa5 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -33,41 +33,48 @@ export type Actions = { */ export class UserSettings extends BaseResource implements UserSettingsResource { id = undefined; - social!: OAuthProviders; - - saml!: SamlSettings; - enterpriseSSO!: EnterpriseSSOSettings; - - attributes!: Attributes; - actions!: Actions; - signIn!: SignInData; - signUp!: SignUpData; - passwordSettings!: PasswordSettingsData; - passkeySettings!: PasskeySettingsData; - usernameSettings!: UsernameSettingsData; + social: OAuthProviders = {} as OAuthProviders; + saml: SamlSettings = {} as SamlSettings; + enterpriseSSO: EnterpriseSSOSettings = {} as EnterpriseSSOSettings; + + attributes: Partial = {}; + actions: Partial = {}; + signIn: SignInData = {} as SignInData; + signUp: SignUpData = {} as SignUpData; + passwordSettings: PasswordSettingsData = {} as PasswordSettingsData; + passkeySettings: PasskeySettingsData = {} as PasskeySettingsData; + usernameSettings: UsernameSettingsData = {} as UsernameSettingsData; socialProviderStrategies: OAuthStrategy[] = []; authenticatableSocialStrategies: OAuthStrategy[] = []; web3FirstFactors: Web3Strategy[] = []; enabledFirstFactorIdentifiers: Array = []; - public constructor(data: UserSettingsJSON | UserSettingsJSONSnapshot) { + /** + * Constructor now accepts an optional data object. + */ + public constructor(data?: UserSettingsJSON | UserSettingsJSONSnapshot | null) { super(); - this.fromJSON(data); + if (data) { + this.fromJSON(data); + } } get instanceIsPasswordBased() { - return this.attributes.password.enabled && this.attributes.password.required; + return Boolean(this.attributes?.password?.enabled && this.attributes.password?.required); } get hasValidAuthFactor() { - return ( - this.attributes.email_address.enabled || - this.attributes.phone_number.enabled || - (this.attributes.password.required && this.attributes.username.required) + return Boolean( + this.attributes?.email_address?.enabled || + this.attributes?.phone_number?.enabled || + (this.attributes.password?.required && this.attributes.username?.required), ); } + /** + * fromJSON now safely returns the instance even if null is provided. + */ protected fromJSON(data: UserSettingsJSON | UserSettingsJSONSnapshot | null): this { if (!data) { return this; @@ -84,22 +91,22 @@ export class UserSettings extends BaseResource implements UserSettingsResource { this.signUp = data.sign_up; this.passwordSettings = { ...data.password_settings, - min_length: Math.max(data?.password_settings?.min_length, defaultMinPasswordLength), + min_length: Math.max(data.password_settings?.min_length, defaultMinPasswordLength), max_length: - data?.password_settings?.max_length === 0 + data.password_settings?.max_length === 0 ? defaultMaxPasswordLength - : Math.min(data?.password_settings?.max_length, defaultMaxPasswordLength), + : Math.min(data.password_settings?.max_length, defaultMaxPasswordLength), }; this.usernameSettings = { ...data.username_settings, - min_length: Math.max(data?.username_settings?.min_length, defaultMinUsernameLength), - max_length: Math.min(data?.username_settings?.max_length, defaultMaxUsernameLength), + min_length: Math.max(data.username_settings?.min_length, defaultMinUsernameLength), + max_length: Math.min(data.username_settings?.max_length, defaultMaxUsernameLength), }; this.passkeySettings = data.passkey_settings; - this.socialProviderStrategies = this.getSocialProviderStrategies(data.social); - this.authenticatableSocialStrategies = this.getAuthenticatableSocialStrategies(data.social); - this.web3FirstFactors = this.getWeb3FirstFactors(this.attributes); - this.enabledFirstFactorIdentifiers = this.getEnabledFirstFactorIdentifiers(this.attributes); + this.socialProviderStrategies = this.getSocialProviderStrategies(); + this.authenticatableSocialStrategies = this.getAuthenticatableSocialStrategies(); + this.web3FirstFactors = this.getWeb3FirstFactors(); + this.enabledFirstFactorIdentifiers = this.getEnabledFirstFactorIdentifiers(); return this; } @@ -117,44 +124,28 @@ export class UserSettings extends BaseResource implements UserSettingsResource { } as unknown as UserSettingsJSONSnapshot; } - private getEnabledFirstFactorIdentifiers(attributes: Attributes): Array { - if (!attributes) { - return []; - } - - return Object.entries(attributes) + private getEnabledFirstFactorIdentifiers(): Array { + return Object.entries(this.attributes) .filter(([name, attr]) => attr.used_for_first_factor && !name.startsWith('web3')) .map(([name]) => name) as Array; } - private getWeb3FirstFactors(attributes: Attributes): Web3Strategy[] { - if (!attributes) { - return []; - } - - return Object.entries(attributes) + private getWeb3FirstFactors(): Web3Strategy[] { + return Object.entries(this.attributes) .filter(([name, attr]) => attr.used_for_first_factor && name.startsWith('web3')) .map(([, desc]) => desc.first_factors) .flat() as any as Web3Strategy[]; } - private getSocialProviderStrategies(social: OAuthProviders): OAuthStrategy[] { - if (!social) { - return []; - } - - return Object.entries(social) + private getSocialProviderStrategies(): OAuthStrategy[] { + return Object.entries(this.social) .filter(([, desc]) => desc.enabled) .map(([, desc]) => desc.strategy) .sort(); } - private getAuthenticatableSocialStrategies(social: OAuthProviders): OAuthStrategy[] { - if (!social) { - return []; - } - - return Object.entries(social) + private getAuthenticatableSocialStrategies(): OAuthStrategy[] { + return Object.entries(this.social) .filter(([, desc]) => desc.enabled && desc.authenticatable) .map(([, desc]) => desc.strategy) .sort(); From ba6f0728ceaf89ad73ee9d9b0a7eb836df041ee2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 5 Mar 2025 11:32:49 -0600 Subject: [PATCH 02/31] handle partial attributes --- .../src/ui/components/SignIn/SignInStart.tsx | 6 +-- .../components/SignUp/SignUpVerifyEmail.tsx | 8 +--- .../ui/components/SignUp/signUpFormHelpers.ts | 45 ++++++++++--------- .../ui/components/UserProfile/AccountPage.tsx | 8 ++-- .../ui/components/UserProfile/EmailForm.tsx | 2 +- .../UserProfile/MfaPhoneCodeScreen.tsx | 2 +- .../ui/components/UserProfile/ProfileForm.tsx | 8 ++-- .../components/UserProfile/SecurityPage.tsx | 2 +- .../components/UserProfile/UsernameForm.tsx | 2 +- .../src/ui/components/UserProfile/utils.ts | 4 +- 10 files changed, 43 insertions(+), 44 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 88ceafd0341..b7d8859b91b 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -49,7 +49,7 @@ const useAutoFillPasskey = () => { await authenticateWithPasskey({ flow: 'autofill' }); } - if (passkeySettings.allow_autofill && attributes.passkey.enabled) { + if (passkeySettings.allow_autofill && attributes.passkey?.enabled) { runAutofillPasskey(); } }, []); @@ -393,7 +393,7 @@ export function _SignInStart(): JSX.Element { signUpMode: userSettings.signUp.mode, redirectUrl, redirectUrlComplete, - passwordEnabled: userSettings.attributes.password.required, + passwordEnabled: userSettings.attributes.password?.required ?? false, }); } else { handleError(e, [identifierField, instantPasswordField], card.setError); @@ -481,7 +481,7 @@ export function _SignInStart(): JSX.Element { ) : null} - {userSettings.attributes.passkey.enabled && + {userSettings.attributes.passkey?.enabled && userSettings.passkeySettings.show_sign_in_button && isWebSupported && ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx index 4057763d3e0..fced8e73920 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx @@ -6,11 +6,7 @@ import { SignUpEmailLinkCard } from './SignUpEmailLinkCard'; export const SignUpVerifyEmail = withCardStateProvider(() => { const { userSettings } = useEnvironment(); const { attributes } = userSettings; - const emailLinkStrategyEnabled = attributes.email_address.verifications.includes('email_link'); + const emailLinkStrategyEnabled = attributes.email_address?.verifications?.includes('email_link'); - if (emailLinkStrategyEnabled) { - return ; - } - - return ; + return emailLinkStrategyEnabled ? : ; }); diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index a2f9ae591c9..e25944d27fb 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -37,7 +37,7 @@ export type Fields = { }; type FieldDeterminationProps = { - attributes: Attributes; + attributes: Partial; activeCommIdentifierType?: ActiveIdentifier; hasTicket?: boolean; hasEmail?: boolean; @@ -103,7 +103,10 @@ export function minimizeFieldsForExistingSignup(fields: Fields, signUp: SignUpRe } } -export const getInitialActiveIdentifier = (attributes: Attributes, isProgressiveSignUp: boolean): ActiveIdentifier => { +export const getInitialActiveIdentifier = ( + attributes: Partial, + isProgressiveSignUp: boolean, +): ActiveIdentifier => { if (emailOrPhone(attributes, isProgressiveSignUp)) { // If we are in the case of Email OR Phone, email takes priority return 'emailAddress'; @@ -111,11 +114,11 @@ export const getInitialActiveIdentifier = (attributes: Attributes, isProgressive const { email_address, phone_number } = attributes; - if (email_address.enabled && isProgressiveSignUp ? email_address.required : email_address.used_for_first_factor) { + if (email_address?.enabled && isProgressiveSignUp ? email_address.required : email_address?.used_for_first_factor) { return 'emailAddress'; } - if (phone_number.enabled && isProgressiveSignUp ? phone_number.required : phone_number.used_for_first_factor) { + if (phone_number?.enabled && isProgressiveSignUp ? phone_number.required : phone_number?.used_for_first_factor) { return 'phoneNumber'; } @@ -128,14 +131,14 @@ export function showFormFields(userSettings: UserSettingsResource): boolean { return userSettings.hasValidAuthFactor || (!authenticatableSocialStrategies.length && !web3FirstFactors.length); } -export function emailOrPhone(attributes: Attributes, isProgressiveSignUp: boolean) { +export function emailOrPhone(attributes: Partial, isProgressiveSignUp: boolean) { const { email_address, phone_number } = attributes; - if (isProgressiveSignUp) { - return email_address.enabled && phone_number.enabled && !email_address.required && !phone_number.required; - } - - return email_address.used_for_first_factor && phone_number.used_for_first_factor; + return Boolean( + isProgressiveSignUp + ? email_address?.enabled && phone_number?.enabled && !email_address.required && !phone_number.required + : email_address?.used_for_first_factor && phone_number?.used_for_first_factor, + ); } function getField(fieldKey: FieldKey, fieldProps: FieldDeterminationProps): Field | undefined { @@ -169,7 +172,7 @@ function getEmailAddressField({ if (isProgressiveSignUp) { // If there is no ticket, or there is a ticket along with an email, and email address is enabled, // we have to show it in the SignUp form - const show = (!hasTicket || (hasTicket && hasEmail)) && attributes.email_address.enabled; + const show = (!hasTicket || (hasTicket && hasEmail)) && attributes.email_address?.enabled; if (!show) { return; @@ -182,15 +185,15 @@ function getEmailAddressField({ } return { - required: attributes.email_address.required, + required: Boolean(attributes.email_address?.required), disabled: !!hasTicket && !!hasEmail, }; } const show = (!hasTicket || (hasTicket && hasEmail)) && - attributes.email_address.enabled && - attributes.email_address.used_for_first_factor && + attributes.email_address?.enabled && + attributes.email_address?.used_for_first_factor && activeCommIdentifierType === 'emailAddress'; if (!show) { @@ -211,7 +214,7 @@ function getPhoneNumberField({ }: FieldDeterminationProps): Field | undefined { if (isProgressiveSignUp) { // If there is no ticket and phone number is enabled, we have to show it in the SignUp form - const show = attributes.phone_number.enabled; + const show = attributes.phone_number?.enabled; if (!show) { return; @@ -224,13 +227,13 @@ function getPhoneNumberField({ } return { - required: attributes.phone_number.required, + required: Boolean(attributes.phone_number?.required), }; } const show = !hasTicket && - attributes.phone_number.enabled && + attributes.phone_number?.enabled && attributes.phone_number.used_for_first_factor && activeCommIdentifierType === 'phoneNumber'; @@ -244,15 +247,15 @@ function getPhoneNumberField({ } // Currently, password is always enabled so only show if required -function getPasswordField(attributes: Attributes): Field | undefined { - const show = attributes.password.enabled && attributes.password.required; +function getPasswordField(attributes: Partial): Field | undefined { + const show = attributes.password?.enabled && attributes.password.required; if (!show) { return; } return { - required: attributes.password.required, + required: Boolean(attributes.password?.required), }; } @@ -276,7 +279,7 @@ function getLegalAcceptedField(legalConsentRequired?: boolean): Field | undefine }; } -function getGenericField(fieldKey: FieldKey, attributes: Attributes): Field | undefined { +function getGenericField(fieldKey: FieldKey, attributes: Partial): Field | undefined { const attrKey = camelToSnake(fieldKey); // @ts-expect-error - TS doesn't know that the key exists diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index 5e0f5694784..8dde4b31b59 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -16,12 +16,12 @@ export const AccountPage = withCardStateProvider(() => { const card = useCardState(); const { user } = useUser(); - const showUsername = attributes.username.enabled; - const showEmail = attributes.email_address.enabled; - const showPhone = attributes.phone_number.enabled; + const showUsername = attributes.username?.enabled; + const showEmail = attributes.email_address?.enabled; + const showPhone = attributes.phone_number?.enabled; const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; const showEnterpriseAccounts = user && enterpriseSSO.enabled; - const showWeb3 = attributes.web3_wallet.enabled; + const showWeb3 = attributes.web3_wallet?.enabled; const shouldAllowIdentificationCreation = !showEnterpriseAccounts || diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx index 43ed2258827..b41659c24b9 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx @@ -125,7 +125,7 @@ const getTranslationKeyByStrategy = (strategy: PrepareEmailAddressVerificationPa function isEmailLinksEnabledForInstance(env: EnvironmentResource): boolean { const { userSettings } = env; const { email_address } = userSettings.attributes; - return email_address.enabled && email_address.verifications.includes('email_link'); + return Boolean(email_address?.enabled && email_address?.verifications.includes('email_link')); } /** diff --git a/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx b/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx index 2bad9c2e64a..c0c0f803c9b 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx @@ -27,7 +27,7 @@ export const MfaPhoneCodeScreen = withCardStateProvider((props: MfaPhoneCodeScre const ref = React.useRef(); const wizard = useWizard({ defaultStep: 2 }); - const isInstanceWithBackupCodes = useEnvironment().userSettings.attributes.backup_code.enabled; + const isInstanceWithBackupCodes = useEnvironment().userSettings.attributes.backup_code?.enabled; return ( diff --git a/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx index a14d4254221..b7c0a01260e 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx @@ -21,8 +21,8 @@ export const ProfileForm = withCardStateProvider((props: ProfileFormProps) => { } const { first_name, last_name } = useEnvironment().userSettings.attributes; - const showFirstName = first_name.enabled; - const showLastName = last_name.enabled; + const showFirstName = first_name?.enabled; + const showLastName = last_name?.enabled; const userFirstName = user.firstName || ''; const userLastName = user.lastName || ''; @@ -30,13 +30,13 @@ export const ProfileForm = withCardStateProvider((props: ProfileFormProps) => { type: 'text', label: localizationKeys('formFieldLabel__firstName'), placeholder: localizationKeys('formFieldInputPlaceholder__firstName'), - isRequired: last_name.required, + isRequired: last_name?.required, }); const lastNameField = useFormControl('lastName', user.lastName || '', { type: 'text', label: localizationKeys('formFieldLabel__lastName'), placeholder: localizationKeys('formFieldInputPlaceholder__lastName'), - isRequired: last_name.required, + isRequired: last_name?.required, }); const userInfoChanged = diff --git a/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx index 5e668d58d53..bd21f0258d2 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx @@ -15,7 +15,7 @@ export const SecurityPage = withCardStateProvider(() => { const card = useCardState(); const { user } = useUser(); const showPassword = instanceIsPasswordBased; - const showPasskey = attributes.passkey.enabled; + const showPasskey = attributes.passkey?.enabled; const showMfa = getSecondFactors(attributes).length > 0; const showDelete = user?.deleteSelfEnabled; diff --git a/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx index 43a36bfd83c..68d46e06830 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx @@ -26,7 +26,7 @@ export const UsernameForm = withCardStateProvider((props: UsernameFormProps) => return null; } - const isUsernameRequired = userSettings.attributes.username.required; + const isUsernameRequired = userSettings.attributes.username?.required; const canSubmit = (isUsernameRequired ? usernameField.value.length > 0 : true) && user.username !== usernameField.value; diff --git a/packages/clerk-js/src/ui/components/UserProfile/utils.ts b/packages/clerk-js/src/ui/components/UserProfile/utils.ts index 10876cd81f4..1fcc8cbc304 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/utils.ts +++ b/packages/clerk-js/src/ui/components/UserProfile/utils.ts @@ -10,7 +10,7 @@ export const currentSessionFirst = (id: string) => (a: IDable) => (a.id === id ? export const defaultFirst = (a: PhoneNumberResource) => (a.defaultSecondFactor ? -1 : 1); -export function getSecondFactors(attributes: Attributes): string[] { +export function getSecondFactors(attributes: Partial): string[] { const secondFactors: string[] = []; Object.entries(attributes).forEach(([, attr]) => { @@ -22,7 +22,7 @@ export function getSecondFactors(attributes: Attributes): string[] { return secondFactors; } -export function getSecondFactorsAvailableToAdd(attributes: Attributes, user: UserResource): string[] { +export function getSecondFactorsAvailableToAdd(attributes: Partial, user: UserResource): string[] { let sfs = getSecondFactors(attributes); // If user.totp_enabled, skip totp from the list of choices From 59c1a011a26fde05d75dfa038cc8c0ef8e2a3ab6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 5 Mar 2025 11:34:08 -0600 Subject: [PATCH 03/31] wip --- .../clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index e25944d27fb..4e83dbaa513 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -283,12 +283,12 @@ function getGenericField(fieldKey: FieldKey, attributes: Partial): F const attrKey = camelToSnake(fieldKey); // @ts-expect-error - TS doesn't know that the key exists - if (!attributes[attrKey].enabled) { + if (!attributes[attrKey]?.enabled) { return; } return { // @ts-expect-error - TS doesn't know that the key exists - required: attributes[attrKey].required, + required: attributes[attrKey]?.required, }; } From 03628a45c3f9b825053c50f73efb50e89e711179 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 5 Mar 2025 12:07:49 -0600 Subject: [PATCH 04/31] expect TS errors for now --- packages/clerk-js/src/core/resources/Environment.ts | 2 ++ packages/clerk-js/src/core/resources/UserSettings.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index ba53a28e11c..9f9e3f35ce6 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -17,6 +17,7 @@ export class Environment extends BaseResource implements EnvironmentResource { pathRoot = '/environment'; authConfig: AuthConfigResource = new AuthConfig(); displayConfig: DisplayConfigResource = new DisplayConfig(); + // @ts-expect-error - This is a partial object, but we want to ensure that all attributes are present. userSettings: UserSettingsResource = new UserSettings(); organizationSettings: OrganizationSettingsResource = new OrganizationSettings(); maintenanceMode: boolean = false; @@ -66,6 +67,7 @@ export class Environment extends BaseResource implements EnvironmentResource { } this.authConfig = new AuthConfig(data.auth_config); + // @ts-expect-error - This is a partial object, but we want to ensure that all attributes are present. this.userSettings = new UserSettings(data.user_settings); this.organizationSettings = new OrganizationSettings(data.organization_settings); this.displayConfig = new DisplayConfig(data.display_config); diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 530111e0fa5..d0cbc47e283 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -37,7 +37,9 @@ export class UserSettings extends BaseResource implements UserSettingsResource { saml: SamlSettings = {} as SamlSettings; enterpriseSSO: EnterpriseSSOSettings = {} as EnterpriseSSOSettings; + // @ts-expect-error - This is a partial object, but we want to ensure that all attributes are present. attributes: Partial = {}; + // @ts-expect-error - This is a partial object, but we want to ensure that all actions are present. actions: Partial = {}; signIn: SignInData = {} as SignInData; signUp: SignUpData = {} as SignUpData; From 650782d362f0f6f8f9a72ed306d830d503485bbb Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 7 Mar 2025 14:32:12 -0600 Subject: [PATCH 05/31] feat: clerk loading status --- packages/clerk-js/src/core/clerk.ts | 5 +++++ packages/types/src/clerk.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index cf8258e7f07..33063279742 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -189,6 +189,7 @@ export class Clerk implements ClerkInterface { #fapiClient: FapiClient; #instanceType?: InstanceType; #loaded = false; + #loadingStatus: ClerkInterface['loadingStatus'] = 'uninitialized'; #listeners: Array<(emission: Resources) => void> = []; #navigationListeners: Array<() => void> = []; @@ -238,6 +239,10 @@ export class Clerk implements ClerkInterface { return this.#loaded; } + get loadingStatus() { + return this.#loadingStatus; + } + get isSatellite(): boolean { if (inBrowser()) { return handleValueOrFn(this.#options.isSatellite, new URL(window.location.href), false); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 427c122e1c6..799d209896f 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -61,6 +61,14 @@ export type SDKMetadata = { environment?: string; }; +export type LoadingStatus = { + DEGRADED: 'degraded'; + ERROR: 'error'; + LOADING: 'loading'; + READY: 'ready'; + UNINITIALIZED: 'uninitialized'; +}; + export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; export type BeforeEmitCallback = (session?: SignedInSessionResource | null) => void | Promise; @@ -105,6 +113,11 @@ export interface Clerk { */ loaded: boolean; + /** + * The current loading status of the Clerk SDK. + */ + loadingStatus: LoadingStatus[keyof LoadingStatus]; + __internal_getOption(key: K): ClerkOptions[K]; frontendApi: string; From 4367db0b1f538c9c93347da8e2fbb94921a0a261 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 09:18:30 -0500 Subject: [PATCH 06/31] update load() --- packages/clerk-js/src/core/clerk.ts | 61 +++++++++++++++++------------ 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 33063279742..9b084ab6f1d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -336,41 +336,52 @@ export class Clerk implements ClerkInterface { public getFapiClient = (): FapiClient => this.#fapiClient; - public load = async (options?: ClerkOptions): Promise => { - if (this.loaded) { + public async load(options?: ClerkOptions): Promise { + if (this.loadingStatus === 'loading') { + logger.warnOnce('Clerk is already loading. Ignoring duplicate call.'); return; } - // Log a development mode warning once - if (this.#instanceType === 'development') { - logger.warnOnce( - 'Clerk: Clerk has been loaded with development keys. Development instances have strict usage limits and should not be used when deploying your application to production. Learn more: https://clerk.com/docs/deployments/overview', - ); + if (this.loadingStatus === 'ready') { + logger.warnOnce('Clerk is already loaded. Skipping load process.'); + return; } - this.#options = this.#initOptions(options); + this.#loadingStatus = 'loading'; - assertNoLegacyProp(this.#options); + try { + if (this.#instanceType === 'development') { + logger.warnOnce('Clerk is running in development mode. Usage limits apply. Do not use in production.'); + } - if (this.#options.sdkMetadata) { - Clerk.sdkMetadata = this.#options.sdkMetadata; - } + this.#options = this.#initOptions(options); + assertNoLegacyProp(this.#options); - if (this.#options.telemetry !== false) { - this.telemetry = new TelemetryCollector({ - clerkVersion: Clerk.version, - samplingRate: 1, - publishableKey: this.publishableKey, - ...this.#options.telemetry, - }); - } + if (this.#options.sdkMetadata) { + Clerk.sdkMetadata = this.#options.sdkMetadata; + } - if (this.#options.standardBrowser) { - this.#loaded = await this.#loadInStandardBrowser(); - } else { - this.#loaded = await this.#loadInNonStandardBrowser(); + if (this.#options.telemetry !== false) { + this.telemetry = new TelemetryCollector({ + clerkVersion: Clerk.version, + samplingRate: 1, + publishableKey: this.publishableKey, + ...this.#options.telemetry, + }); + } + + if (this.#options.standardBrowser) { + this.#loaded = await this.#loadInStandardBrowser(); + } else { + this.#loaded = await this.#loadInNonStandardBrowser(); + } + + this.#loadingStatus = 'ready'; + } catch (error) { + this.#loadingStatus = 'error'; + throw error; } - }; + } #isCombinedSignInOrUpFlow(): boolean { return Boolean(!this.#options.signUpUrl && this.#options.signInUrl && !isAbsoluteUrl(this.#options.signInUrl)); From b27298ee99b76945e137b19f3cd11d9f03c47167 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 11:24:02 -0500 Subject: [PATCH 07/31] wip --- packages/clerk-js/src/core/clerk.ts | 16 ++++++++-------- packages/types/src/clerk.ts | 10 ++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 9b084ab6f1d..586a5818cb5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -189,7 +189,7 @@ export class Clerk implements ClerkInterface { #fapiClient: FapiClient; #instanceType?: InstanceType; #loaded = false; - #loadingStatus: ClerkInterface['loadingStatus'] = 'uninitialized'; + #status: ClerkInterface['status'] = 'uninitialized'; #listeners: Array<(emission: Resources) => void> = []; #navigationListeners: Array<() => void> = []; @@ -239,8 +239,8 @@ export class Clerk implements ClerkInterface { return this.#loaded; } - get loadingStatus() { - return this.#loadingStatus; + get status() { + return this.#status; } get isSatellite(): boolean { @@ -337,17 +337,17 @@ export class Clerk implements ClerkInterface { public getFapiClient = (): FapiClient => this.#fapiClient; public async load(options?: ClerkOptions): Promise { - if (this.loadingStatus === 'loading') { + if (this.status === 'loading') { logger.warnOnce('Clerk is already loading. Ignoring duplicate call.'); return; } - if (this.loadingStatus === 'ready') { + if (this.status === 'ready') { logger.warnOnce('Clerk is already loaded. Skipping load process.'); return; } - this.#loadingStatus = 'loading'; + this.#status = 'loading'; try { if (this.#instanceType === 'development') { @@ -376,9 +376,9 @@ export class Clerk implements ClerkInterface { this.#loaded = await this.#loadInNonStandardBrowser(); } - this.#loadingStatus = 'ready'; + this.#status = 'ready'; } catch (error) { - this.#loadingStatus = 'error'; + this.#status = 'error'; throw error; } } diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index e0c646be0d0..5593ebf48b2 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -60,13 +60,7 @@ export type SDKMetadata = { environment?: string; }; -export type LoadingStatus = { - DEGRADED: 'degraded'; - ERROR: 'error'; - LOADING: 'loading'; - READY: 'ready'; - UNINITIALIZED: 'uninitialized'; -}; +export type Status = 'degraded' | 'error' | 'loading' | 'ready' | 'uninitialized'; export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; @@ -115,7 +109,7 @@ export interface Clerk { /** * The current loading status of the Clerk SDK. */ - loadingStatus: LoadingStatus[keyof LoadingStatus]; + status: Status; /** * @internal From 959fb0e8783dbf06438c153c664566b2c31d0dda Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 11:43:52 -0500 Subject: [PATCH 08/31] wip --- packages/clerk-js/src/core/clerk.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 586a5818cb5..cc44159d13d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -375,6 +375,7 @@ export class Clerk implements ClerkInterface { } else { this.#loaded = await this.#loadInNonStandardBrowser(); } + if (this.#loaded === false) throw new Error('Clerk failed to load'); this.#status = 'ready'; } catch (error) { From d1560384321a02f055a45c1dddd71ce99b9690a6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 18:55:11 -0500 Subject: [PATCH 09/31] wip --- packages/clerk-js/src/core/clerk.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index cc44159d13d..297fc54d9cf 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -188,7 +188,6 @@ export class Clerk implements ClerkInterface { //@ts-expect-error with being undefined even though it's not possible - related to issue with ts and error thrower #fapiClient: FapiClient; #instanceType?: InstanceType; - #loaded = false; #status: ClerkInterface['status'] = 'uninitialized'; #listeners: Array<(emission: Resources) => void> = []; @@ -236,7 +235,7 @@ export class Clerk implements ClerkInterface { } get loaded(): boolean { - return this.#loaded; + return this.#status === 'ready'; } get status() { @@ -370,12 +369,11 @@ export class Clerk implements ClerkInterface { }); } - if (this.#options.standardBrowser) { - this.#loaded = await this.#loadInStandardBrowser(); - } else { - this.#loaded = await this.#loadInNonStandardBrowser(); - } - if (this.#loaded === false) throw new Error('Clerk failed to load'); + const loaded = this.#options.standardBrowser + ? await this.#loadInStandardBrowser() + : await this.#loadInNonStandardBrowser(); + + if (loaded === false) throw new Error('Clerk failed to load'); this.#status = 'ready'; } catch (error) { From 4efe1a5072462bfbb87ffb5db78176b11c2b9cf8 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 19:10:21 -0500 Subject: [PATCH 10/31] isomorphic clerk status --- packages/react/src/isomorphicClerk.ts | 161 ++++++++++++++------------ 1 file changed, 85 insertions(+), 76 deletions(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 85202601e3d..e4bec4f6151 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -1,4 +1,3 @@ -import { inBrowser } from '@clerk/shared/browser'; import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import { handleValueOrFn } from '@clerk/shared/utils'; import type { @@ -124,17 +123,27 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { >(); private loadedListeners: Array<() => void> = []; - #loaded = false; #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; #publishableKey: string; + /** + * @private + * @property {LoadedClerk['status']} #status - Represents the current status of the Clerk instance. + * The status is initialized to 'uninitialized' and can be updated to reflect the current state. + */ + #status: LoadedClerk['status'] = 'uninitialized'; + get publishableKey(): string { return this.#publishableKey; } get loaded(): boolean { - return this.#loaded; + return this.#status === 'ready'; + } + + get status() { + return this.#status; } static #instance: IsomorphicClerk | null | undefined; @@ -145,7 +154,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { // Also will recreate the instance if the provided Clerk instance changes // This method should be idempotent in both scenarios if ( - !inBrowser() || + typeof window === 'undefined' || !this.#instance || (options.Clerk && this.#instance.Clerk !== options.Clerk) || // Allow hot swapping PKs on the client @@ -195,7 +204,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.#domain = options?.domain; this.options = options; this.Clerk = Clerk; - this.mode = inBrowser() ? 'browser' : 'server'; + this.mode = typeof window === 'undefined' ? 'server' : 'browser'; if (!this.options.sdkMetadata) { this.options.sdkMetadata = SDK_METADATA; @@ -236,7 +245,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildSignInUrl = (opts?: RedirectOptions): string | void => { const callback = () => this.clerkjs?.buildSignInUrl(opts) || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildSignInUrl', callback); @@ -245,7 +254,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildSignUpUrl = (opts?: RedirectOptions): string | void => { const callback = () => this.clerkjs?.buildSignUpUrl(opts) || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildSignUpUrl', callback); @@ -254,7 +263,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildAfterSignInUrl = (...args: Parameters): string | void => { const callback = () => this.clerkjs?.buildAfterSignInUrl(...args) || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildAfterSignInUrl', callback); @@ -263,7 +272,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildAfterSignUpUrl = (...args: Parameters): string | void => { const callback = () => this.clerkjs?.buildAfterSignUpUrl(...args) || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildAfterSignUpUrl', callback); @@ -272,7 +281,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildAfterSignOutUrl = (): string | void => { const callback = () => this.clerkjs?.buildAfterSignOutUrl() || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildAfterSignOutUrl', callback); @@ -281,7 +290,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildAfterMultiSessionSingleSignOutUrl = (): string | void => { const callback = () => this.clerkjs?.buildAfterMultiSessionSingleSignOutUrl() || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildAfterMultiSessionSingleSignOutUrl', callback); @@ -290,7 +299,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildUserProfileUrl = (): string | void => { const callback = () => this.clerkjs?.buildUserProfileUrl() || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildUserProfileUrl', callback); @@ -299,7 +308,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildCreateOrganizationUrl = (): string | void => { const callback = () => this.clerkjs?.buildCreateOrganizationUrl() || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildCreateOrganizationUrl', callback); @@ -308,7 +317,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildOrganizationProfileUrl = (): string | void => { const callback = () => this.clerkjs?.buildOrganizationProfileUrl() || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildOrganizationProfileUrl', callback); @@ -317,7 +326,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildWaitlistUrl = (): string | void => { const callback = () => this.clerkjs?.buildWaitlistUrl() || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildWaitlistUrl', callback); @@ -326,7 +335,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { buildUrlWithAuth = (to: string): string | void => { const callback = () => this.clerkjs?.buildUrlWithAuth(to) || ''; - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('buildUrlWithAuth', callback); @@ -335,7 +344,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { handleUnauthenticated = async () => { const callback = () => this.clerkjs?.handleUnauthenticated(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { void callback(); } else { this.premountMethodCalls.set('handleUnauthenticated', callback); @@ -349,7 +358,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } async loadClerkJS(): Promise { - if (this.mode !== 'browser' || this.#loaded) { + if (this.mode !== 'browser' || this.loaded) { return; } @@ -512,7 +521,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.mountWaitlist(node, props); }); - this.#loaded = true; + this.#status = 'ready'; this.emitLoaded(); return this.clerkjs; }; @@ -607,7 +616,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openSignIn = (props?: SignInProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openSignIn(props); } else { this.preopenSignIn = props; @@ -615,7 +624,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeSignIn = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeSignIn(); } else { this.preopenSignIn = null; @@ -623,7 +632,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; __internal_openReverification = (props?: __internal_UserVerificationModalProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.__internal_openReverification(props); } else { this.preopenUserVerification = props; @@ -631,7 +640,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; __internal_closeReverification = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.__internal_closeReverification(); } else { this.preopenUserVerification = null; @@ -639,7 +648,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openGoogleOneTap = (props?: GoogleOneTapProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openGoogleOneTap(props); } else { this.preopenOneTap = props; @@ -647,7 +656,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeGoogleOneTap = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeGoogleOneTap(); } else { this.preopenOneTap = null; @@ -655,7 +664,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openUserProfile = (props?: UserProfileProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openUserProfile(props); } else { this.preopenUserProfile = props; @@ -663,7 +672,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeUserProfile = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeUserProfile(); } else { this.preopenUserProfile = null; @@ -671,7 +680,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openOrganizationProfile = (props?: OrganizationProfileProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openOrganizationProfile(props); } else { this.preopenOrganizationProfile = props; @@ -679,7 +688,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeOrganizationProfile = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeOrganizationProfile(); } else { this.preopenOrganizationProfile = null; @@ -687,7 +696,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openCreateOrganization = (props?: CreateOrganizationProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openCreateOrganization(props); } else { this.preopenCreateOrganization = props; @@ -695,7 +704,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeCreateOrganization = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeCreateOrganization(); } else { this.preopenCreateOrganization = null; @@ -703,7 +712,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openWaitlist = (props?: WaitlistProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openWaitlist(props); } else { this.preOpenWaitlist = props; @@ -711,7 +720,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeWaitlist = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeWaitlist(); } else { this.preOpenWaitlist = null; @@ -719,7 +728,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; openSignUp = (props?: SignUpProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.openSignUp(props); } else { this.preopenSignUp = props; @@ -727,7 +736,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; closeSignUp = () => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.closeSignUp(); } else { this.preopenSignUp = null; @@ -735,7 +744,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountSignIn = (node: HTMLDivElement, props?: SignInProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountSignIn(node, props); } else { this.premountSignInNodes.set(node, props); @@ -743,7 +752,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountSignIn = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountSignIn(node); } else { this.premountSignInNodes.delete(node); @@ -751,7 +760,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountSignUp = (node: HTMLDivElement, props?: SignUpProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountSignUp(node, props); } else { this.premountSignUpNodes.set(node, props); @@ -759,7 +768,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountSignUp = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountSignUp(node); } else { this.premountSignUpNodes.delete(node); @@ -767,7 +776,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountUserProfile = (node: HTMLDivElement, props?: UserProfileProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountUserProfile(node, props); } else { this.premountUserProfileNodes.set(node, props); @@ -775,7 +784,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountUserProfile = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountUserProfile(node); } else { this.premountUserProfileNodes.delete(node); @@ -783,7 +792,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountOrganizationProfile(node, props); } else { this.premountOrganizationProfileNodes.set(node, props); @@ -791,7 +800,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountOrganizationProfile = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountOrganizationProfile(node); } else { this.premountOrganizationProfileNodes.delete(node); @@ -799,7 +808,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountCreateOrganization(node, props); } else { this.premountCreateOrganizationNodes.set(node, props); @@ -807,7 +816,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountCreateOrganization = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountCreateOrganization(node); } else { this.premountCreateOrganizationNodes.delete(node); @@ -815,7 +824,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountOrganizationSwitcher(node, props); } else { this.premountOrganizationSwitcherNodes.set(node, props); @@ -823,7 +832,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountOrganizationSwitcher = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountOrganizationSwitcher(node); } else { this.premountOrganizationSwitcherNodes.delete(node); @@ -832,7 +841,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { __experimental_prefetchOrganizationSwitcher = () => { const callback = () => this.clerkjs?.__experimental_prefetchOrganizationSwitcher(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { void callback(); } else { this.premountMethodCalls.set('__experimental_prefetchOrganizationSwitcher', callback); @@ -840,7 +849,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountOrganizationList(node, props); } else { this.premountOrganizationListNodes.set(node, props); @@ -848,7 +857,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountOrganizationList = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountOrganizationList(node); } else { this.premountOrganizationListNodes.delete(node); @@ -856,7 +865,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountUserButton = (node: HTMLDivElement, userButtonProps?: UserButtonProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountUserButton(node, userButtonProps); } else { this.premountUserButtonNodes.set(node, userButtonProps); @@ -864,7 +873,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountUserButton = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountUserButton(node); } else { this.premountUserButtonNodes.delete(node); @@ -872,7 +881,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; mountWaitlist = (node: HTMLDivElement, props?: WaitlistProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.mountWaitlist(node, props); } else { this.premountWaitlistNodes.set(node, props); @@ -880,7 +889,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; unmountWaitlist = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.unmountWaitlist(node); } else { this.premountWaitlistNodes.delete(node); @@ -905,7 +914,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { navigate = (to: string) => { const callback = () => this.clerkjs?.navigate(to); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { void callback(); } else { this.premountMethodCalls.set('navigate', callback); @@ -914,7 +923,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectWithAuth = async (...args: Parameters) => { const callback = () => this.clerkjs?.redirectWithAuth(...args); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectWithAuth', callback); @@ -924,7 +933,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToSignIn = async (opts?: SignInRedirectOptions) => { const callback = () => this.clerkjs?.redirectToSignIn(opts as any); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToSignIn', callback); @@ -934,7 +943,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToSignUp = async (opts?: SignUpRedirectOptions) => { const callback = () => this.clerkjs?.redirectToSignUp(opts as any); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToSignUp', callback); @@ -944,7 +953,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToUserProfile = async () => { const callback = () => this.clerkjs?.redirectToUserProfile(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToUserProfile', callback); @@ -954,7 +963,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToAfterSignUp = (): void => { const callback = () => this.clerkjs?.redirectToAfterSignUp(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToAfterSignUp', callback); @@ -963,7 +972,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToAfterSignIn = () => { const callback = () => this.clerkjs?.redirectToAfterSignIn(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { callback(); } else { this.premountMethodCalls.set('redirectToAfterSignIn', callback); @@ -972,7 +981,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToAfterSignOut = () => { const callback = () => this.clerkjs?.redirectToAfterSignOut(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { callback(); } else { this.premountMethodCalls.set('redirectToAfterSignOut', callback); @@ -981,7 +990,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToOrganizationProfile = async () => { const callback = () => this.clerkjs?.redirectToOrganizationProfile(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToOrganizationProfile', callback); @@ -991,7 +1000,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToCreateOrganization = async () => { const callback = () => this.clerkjs?.redirectToCreateOrganization(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToCreateOrganization', callback); @@ -1001,7 +1010,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { redirectToWaitlist = async () => { const callback = () => this.clerkjs?.redirectToWaitlist(); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback(); } else { this.premountMethodCalls.set('redirectToWaitlist', callback); @@ -1011,7 +1020,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { handleRedirectCallback = async (params: HandleOAuthCallbackParams): Promise => { const callback = () => this.clerkjs?.handleRedirectCallback(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { void callback()?.catch(() => { // This error is caused when the host app is using React18 // and strictMode is enabled. This useEffects runs twice because @@ -1031,7 +1040,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { params: HandleOAuthCallbackParams, ): Promise => { const callback = () => this.clerkjs?.handleGoogleOneTapCallback(signInOrUp, params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { void callback()?.catch(() => { // This error is caused when the host app is using React18 // and strictMode is enabled. This useEffects runs twice because @@ -1048,7 +1057,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { handleEmailLinkVerification = async (params: HandleEmailLinkVerificationParams) => { const callback = () => this.clerkjs?.handleEmailLinkVerification(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('handleEmailLinkVerification', callback); @@ -1057,7 +1066,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { authenticateWithMetamask = async (params?: AuthenticateWithMetamaskParams) => { const callback = () => this.clerkjs?.authenticateWithMetamask(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('authenticateWithMetamask', callback); @@ -1066,7 +1075,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { authenticateWithCoinbaseWallet = async (params?: AuthenticateWithCoinbaseWalletParams) => { const callback = () => this.clerkjs?.authenticateWithCoinbaseWallet(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('authenticateWithCoinbaseWallet', callback); @@ -1075,7 +1084,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { authenticateWithOKXWallet = async (params?: AuthenticateWithOKXWalletParams) => { const callback = () => this.clerkjs?.authenticateWithOKXWallet(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('authenticateWithOKXWallet', callback); @@ -1084,7 +1093,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { authenticateWithWeb3 = async (params: ClerkAuthenticateWithWeb3Params) => { const callback = () => this.clerkjs?.authenticateWithWeb3(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('authenticateWithWeb3', callback); @@ -1098,7 +1107,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { createOrganization = async (params: CreateOrganizationParams): Promise => { const callback = () => this.clerkjs?.createOrganization(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('createOrganization', callback); @@ -1107,7 +1116,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { getOrganization = async (organizationId: string): Promise => { const callback = () => this.clerkjs?.getOrganization(organizationId); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('getOrganization', callback); @@ -1116,7 +1125,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { joinWaitlist = async (params: JoinWaitlistParams): Promise => { const callback = () => this.clerkjs?.joinWaitlist(params); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('joinWaitlist', callback); @@ -1125,7 +1134,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { signOut = async (...args: Parameters) => { const callback = () => this.clerkjs?.signOut(...args); - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { return callback() as Promise; } else { this.premountMethodCalls.set('signOut', callback); From 68b7161d44464f61848d5f34a006630643f98d79 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 20:06:43 -0500 Subject: [PATCH 11/31] wip --- .../src/contexts/ClerkContextProvider.tsx | 31 ++++++++++--------- packages/react/src/isomorphicClerk.ts | 2 +- packages/shared/src/loadClerkJsScript.ts | 8 ++--- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 188bc2b3a5e..271eb1ae0f7 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -29,7 +29,7 @@ export function ClerkContextProvider(props: ClerkContextProvider) { React.useEffect(() => { return clerk.addListener(e => setState({ ...e })); - }, []); + }, [clerk]); const derivedState = deriveState(clerkLoaded, state, initialState); const clerkCtx = React.useMemo(() => ({ value: clerk }), [clerkLoaded]); @@ -49,19 +49,22 @@ export function ClerkContextProvider(props: ClerkContextProvider) { factorVerificationAge, } = derivedState; - const authCtx = React.useMemo(() => { - const value = { - sessionId, - userId, - actor, - orgId, - orgRole, - orgSlug, - orgPermissions, - factorVerificationAge, - }; - return { value }; - }, [sessionId, userId, actor, orgId, orgRole, orgSlug, factorVerificationAge]); + const authCtx = React.useMemo( + () => ({ + value: { + sessionId, + userId, + actor, + orgId, + orgRole, + orgSlug, + orgPermissions, + factorVerificationAge, + }, + }), + [sessionId, userId, actor, orgId, orgRole, orgSlug, factorVerificationAge], + ); + const sessionCtx = React.useMemo(() => ({ value: session }), [sessionId, session]); const userCtx = React.useMemo(() => ({ value: user }), [userId, user]); const organizationCtx = React.useMemo(() => { diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index e4bec4f6151..3a4feb7806f 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -198,7 +198,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } constructor(options: IsomorphicClerkOptions) { - const { Clerk = null, publishableKey } = options || {}; + const { Clerk = null, publishableKey } = options ?? {}; this.#publishableKey = publishableKey; this.#proxyUrl = options?.proxyUrl; this.#domain = options?.domain; diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 97647e3f0bd..24e047ef186 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -57,7 +57,7 @@ const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions) => { }); existingScript.addEventListener('error', () => { - reject(FAILED_TO_LOAD_ERROR); + reject(new Error(FAILED_TO_LOAD_ERROR)); }); }); } @@ -133,9 +133,9 @@ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => { const applyClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => (script: HTMLScriptElement) => { const attributes = buildClerkJsScriptAttributes(options); - for (const attribute in attributes) { - script.setAttribute(attribute, attributes[attribute]); - } + Object.entries(attributes).forEach(([key, value]) => { + script.setAttribute(key, value); + }); }; export { loadClerkJsScript, buildClerkJsScriptAttributes, clerkJsScriptUrl }; From 1c192143cadfa762e1d84d0f3b38599b84e56a76 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 21:43:02 -0500 Subject: [PATCH 12/31] add simple event emitter --- packages/react/src/isomorphicClerk.ts | 20 +-- packages/react/src/utils/EventEmitter.ts | 66 ++++++++++ .../src/utils/__tests__/EventEmitter.test.ts | 114 ++++++++++++++++++ 3 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 packages/react/src/utils/EventEmitter.ts create mode 100644 packages/react/src/utils/__tests__/EventEmitter.test.ts diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 3a4feb7806f..43d965aa69e 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -51,6 +51,7 @@ import type { IsomorphicClerkOptions, } from './types'; import { isConstructor } from './utils'; +import { EventEmitter } from './utils/EventEmitter'; if (typeof globalThis.__BUILD_DISABLE_RHC__ === 'undefined') { globalThis.__BUILD_DISABLE_RHC__ = false; @@ -95,6 +96,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private readonly options: IsomorphicClerkOptions; private readonly Clerk: ClerkProp; private clerkjs: BrowserClerk | HeadlessBrowserClerk | null = null; + private eventEmitter = new EventEmitter(); private preopenOneTap?: null | GoogleOneTapProps = null; private preopenUserVerification?: null | __internal_UserVerificationProps = null; private preopenSignIn?: null | SignInProps = null; @@ -439,19 +441,19 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } public addOnLoaded = (cb: () => void) => { - this.loadedListeners.push(cb); - /** - * When IsomorphicClerk is loaded execute the callback directly - */ + this.eventEmitter.on('loaded', cb); if (this.loaded) { - this.emitLoaded(); + this.eventEmitter.emit('loaded'); } }; - public emitLoaded = () => { - this.loadedListeners.forEach(cb => cb()); - this.loadedListeners = []; - }; + public emitLoaded() { + this.eventEmitter.emit('loaded'); + } + + public removeOnLoaded(cb: () => void) { + this.eventEmitter.off('loaded', cb); + } private hydrateClerkJS = (clerkjs: BrowserClerk | HeadlessBrowserClerk | undefined) => { if (!clerkjs) { diff --git a/packages/react/src/utils/EventEmitter.ts b/packages/react/src/utils/EventEmitter.ts new file mode 100644 index 00000000000..85dbc3c9646 --- /dev/null +++ b/packages/react/src/utils/EventEmitter.ts @@ -0,0 +1,66 @@ +/** + * A simple event emitter class for managing event-driven behavior. + */ +export class EventEmitter { + /** + * Stores event listeners, mapping event names to sets of callback functions. + */ + private events: Map void>> = new Map(); + + /** + * Registers a new listener for a specific event. + * + * @param event - The name of the event to listen for. + * @param listener - The callback function to execute when the event is emitted. + */ + on(event: string, listener: (...args: any[]) => void): void { + let listeners = this.events.get(event); + if (!listeners) { + listeners = new Set(); + this.events.set(event, listeners); + } + listeners.add(listener); + } + + /** + * Removes a specific listener from an event. + * + * @param event - The name of the event. + * @param listener - The callback function to remove. + */ + off(event: string, listener: (...args: any[]) => void): void { + const listeners = this.events.get(event); + if (listeners) { + listeners.delete(listener); + if (listeners.size === 0) { + this.events.delete(event); + } + } + } + + /** + * Emits an event, calling all registered listeners for that event. + * + * @param event - The name of the event to emit. + * @param args - Optional arguments to pass to the event listeners. + */ + emit(event: string, ...args: any[]): void { + const listeners = this.events.get(event); + if (listeners) { + listeners.forEach(listener => listener(...args)); + } + } + + /** + * Removes all listeners for a specific event or clears all events if no event name is provided. + * + * @param event - The name of the event to clear. If omitted, all events will be cleared. + */ + clear(event?: string): void { + if (event) { + this.events.delete(event); + } else { + this.events.clear(); + } + } +} diff --git a/packages/react/src/utils/__tests__/EventEmitter.test.ts b/packages/react/src/utils/__tests__/EventEmitter.test.ts new file mode 100644 index 00000000000..664690e1ab6 --- /dev/null +++ b/packages/react/src/utils/__tests__/EventEmitter.test.ts @@ -0,0 +1,114 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { EventEmitter } from '../EventEmitter'; + +describe('EventEmitter', () => { + let emitter: EventEmitter; + + beforeEach(() => { + emitter = new EventEmitter(); + }); + + it('calls event listeners when an event is emitted', () => { + const callback = vi.fn(); + emitter.on('testEvent', callback); + emitter.emit('testEvent'); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('passes arguments to event listeners', () => { + const callback = vi.fn(); + emitter.on('dataEvent', callback); + emitter.emit('dataEvent', 'hello', 42); + + expect(callback).toHaveBeenCalledWith('hello', 42); + }); + + it('supports multiple listeners for the same event', () => { + const cb1 = vi.fn(); + const cb2 = vi.fn(); + + emitter.on('multiEvent', cb1); + emitter.on('multiEvent', cb2); + emitter.emit('multiEvent'); + + expect(cb1).toHaveBeenCalledTimes(1); + expect(cb2).toHaveBeenCalledTimes(1); + }); + + it('removes a specific listener and prevents it from being called', () => { + const callback = vi.fn(); + emitter.on('removeEvent', callback); + emitter.off('removeEvent', callback); + emitter.emit('removeEvent'); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('removes all listeners for a given event', () => { + const cb1 = vi.fn(); + const cb2 = vi.fn(); + + emitter.on('clearEvent', cb1); + emitter.on('clearEvent', cb2); + emitter.clear('clearEvent'); + emitter.emit('clearEvent'); + + expect(cb1).not.toHaveBeenCalled(); + expect(cb2).not.toHaveBeenCalled(); + }); + + it('clears all listeners when no event is specified', () => { + const cb1 = vi.fn(); + const cb2 = vi.fn(); + + emitter.on('event1', cb1); + emitter.on('event2', cb2); + emitter.clear(); + emitter.emit('event1'); + emitter.emit('event2'); + + expect(cb1).not.toHaveBeenCalled(); + expect(cb2).not.toHaveBeenCalled(); + }); + + it('ignores removal of a non-existent listener', () => { + const callback = vi.fn(); + emitter.off('nonExistentEvent', callback); + emitter.emit('nonExistentEvent'); + + expect(callback).not.toHaveBeenCalled(); // Ensures no crash or unwanted behavior + }); + + it('handles emitting an event with no listeners gracefully', () => { + expect(() => emitter.emit('emptyEvent')).not.toThrow(); + }); + + it('removes a listener while executing the event without affecting execution', () => { + const callback = vi.fn(() => { + emitter.off('selfRemoveEvent', callback); + }); + + emitter.on('selfRemoveEvent', callback); + emitter.emit('selfRemoveEvent'); + emitter.emit('selfRemoveEvent'); // Should not call callback again + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('allows multiple removals of the same listener without breaking', () => { + const cb1 = vi.fn(); + const cb2 = vi.fn(); + + emitter.on('doubleRemoveEvent', cb1); + emitter.on('doubleRemoveEvent', cb2); + emitter.off('doubleRemoveEvent', cb1); + emitter.off('doubleRemoveEvent', cb1); // Removing twice shouldn't cause issues + + emitter.emit('doubleRemoveEvent'); + + expect(cb1).not.toHaveBeenCalled(); + expect(cb2).toHaveBeenCalledTimes(1); + }); +}); From b186a101c1d2085ddf2c6157452ae2e135cf8372 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 21:49:30 -0500 Subject: [PATCH 13/31] add once() to eventemitter --- packages/react/src/utils/EventEmitter.ts | 22 ++++++- .../src/utils/__tests__/EventEmitter.test.ts | 57 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/react/src/utils/EventEmitter.ts b/packages/react/src/utils/EventEmitter.ts index 85dbc3c9646..080afdd9f29 100644 --- a/packages/react/src/utils/EventEmitter.ts +++ b/packages/react/src/utils/EventEmitter.ts @@ -22,6 +22,25 @@ export class EventEmitter { listeners.add(listener); } + /** + * Registers a one-time listener for a specific event. + * The listener is automatically removed after being called once. + * + * @param event - The name of the event to listen for. + * @param listener - The callback function to execute when the event is emitted. + */ + once(event: string, listener: (...args: any[]) => void): void { + const onceWrapper = (...args: any[]) => { + this.off(event, onceWrapper); // Remove after first execution + listener(...args); + }; + + // Store a reference mapping for proper removal + Object.defineProperty(listener, '__onceWrapper', { value: onceWrapper }); + + this.on(event, onceWrapper); + } + /** * Removes a specific listener from an event. * @@ -31,7 +50,8 @@ export class EventEmitter { off(event: string, listener: (...args: any[]) => void): void { const listeners = this.events.get(event); if (listeners) { - listeners.delete(listener); + const wrappedListener = (listener as any).__onceWrapper || listener; + listeners.delete(wrappedListener); if (listeners.size === 0) { this.events.delete(event); } diff --git a/packages/react/src/utils/__tests__/EventEmitter.test.ts b/packages/react/src/utils/__tests__/EventEmitter.test.ts index 664690e1ab6..2433d80d472 100644 --- a/packages/react/src/utils/__tests__/EventEmitter.test.ts +++ b/packages/react/src/utils/__tests__/EventEmitter.test.ts @@ -111,4 +111,61 @@ describe('EventEmitter', () => { expect(cb1).not.toHaveBeenCalled(); expect(cb2).toHaveBeenCalledTimes(1); }); + + describe('once', () => { + it('executes a one-time listener only once', () => { + const callback = vi.fn(); + emitter.once('onceEvent', callback); + + emitter.emit('onceEvent'); + emitter.emit('onceEvent'); // Should not trigger again + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('passes arguments to a one-time listener', () => { + const callback = vi.fn(); + emitter.once('onceArgsEvent', callback); + + emitter.emit('onceArgsEvent', 'hello', 42); + + expect(callback).toHaveBeenCalledWith('hello', 42); + }); + + it('removes a one-time listener automatically after execution', () => { + const callback = vi.fn(); + emitter.once('autoRemoveEvent', callback); + + emitter.emit('autoRemoveEvent'); + + // Emitting again should not call the callback + emitter.emit('autoRemoveEvent'); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('does not affect other listeners on the same event', () => { + const onceCallback = vi.fn(); + const regularCallback = vi.fn(); + + emitter.once('mixedEvent', onceCallback); + emitter.on('mixedEvent', regularCallback); + + emitter.emit('mixedEvent'); + emitter.emit('mixedEvent'); // Only `regularCallback` should fire again + + expect(onceCallback).toHaveBeenCalledTimes(1); + expect(regularCallback).toHaveBeenCalledTimes(2); + }); + + it('does not call the one-time listener if removed before execution', () => { + const callback = vi.fn(); + emitter.once('removeBeforeEvent', callback); + + emitter.off('removeBeforeEvent', callback); + emitter.emit('removeBeforeEvent'); + + expect(callback).not.toHaveBeenCalled(); + }); + }); }); From 1e6e07d44daa354d887a055eb91cf83368aef653 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 10 Mar 2025 21:53:35 -0500 Subject: [PATCH 14/31] use once --- packages/react/src/isomorphicClerk.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 43d965aa69e..aa2a2d1e936 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -441,16 +441,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } public addOnLoaded = (cb: () => void) => { - this.eventEmitter.on('loaded', cb); + this.eventEmitter.once('loaded', cb); if (this.loaded) { this.eventEmitter.emit('loaded'); } }; - public emitLoaded() { - this.eventEmitter.emit('loaded'); - } - public removeOnLoaded(cb: () => void) { this.eventEmitter.off('loaded', cb); } @@ -524,7 +520,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }); this.#status = 'ready'; - this.emitLoaded(); + this.eventEmitter.emit('loaded'); return this.clerkjs; }; From 2f69bcbc17edd732fc2fe7df4596956827149e38 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 11 Mar 2025 10:18:13 -0500 Subject: [PATCH 15/31] wip --- packages/react/src/isomorphicClerk.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index aa2a2d1e936..0068eacb516 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -360,7 +360,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } async loadClerkJS(): Promise { - if (this.mode !== 'browser' || this.loaded) { + if (typeof window === 'undefined' || this.loaded) { return; } @@ -374,11 +374,9 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { // For more information refer to: // - https://github.com/remix-run/remix/issues/2947 // - https://github.com/facebook/react/issues/24430 - if (typeof window !== 'undefined') { - window.__clerk_publishable_key = this.#publishableKey; - window.__clerk_proxy_url = this.proxyUrl; - window.__clerk_domain = this.domain; - } + window.__clerk_publishable_key = this.#publishableKey; + window.__clerk_proxy_url = this.proxyUrl; + window.__clerk_domain = this.domain; try { if (this.Clerk) { From c09241eb590156de61e8b890fdbc397e0c264163 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 18 Mar 2025 13:55:59 -0500 Subject: [PATCH 16/31] changeset --- .changeset/twenty-eggs-post.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/twenty-eggs-post.md diff --git a/.changeset/twenty-eggs-post.md b/.changeset/twenty-eggs-post.md new file mode 100644 index 00000000000..5fa5f7323ed --- /dev/null +++ b/.changeset/twenty-eggs-post.md @@ -0,0 +1,8 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +Introducing granular Clerk loading status From cc926d875c33f0ab505cb1f4f7c242a0ddf6a3fa Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 18 Mar 2025 14:14:47 -0500 Subject: [PATCH 17/31] fix build --- packages/react/src/isomorphicClerk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index ddf42eeb6cf..adb6d5ac442 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -773,7 +773,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.__experimental_mountPricingTable(node, props); } else { this.premountPricingTableNodes.set(node, props); @@ -781,7 +781,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; __experimental_unmountPricingTable = (node: HTMLDivElement) => { - if (this.clerkjs && this.#loaded) { + if (this.clerkjs && this.loaded) { this.clerkjs.__experimental_unmountPricingTable(node); } else { this.premountPricingTableNodes.delete(node); From d329df8c5247ee4bc408db60d9bec125ef2a7848 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 18 Mar 2025 14:19:02 -0500 Subject: [PATCH 18/31] wip --- .../clerk-js/src/core/resources/OrganizationSettings.ts | 2 -- packages/clerk-js/src/core/resources/UserSettings.ts | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationSettings.ts b/packages/clerk-js/src/core/resources/OrganizationSettings.ts index cc847211a4b..49504ba5279 100644 --- a/packages/clerk-js/src/core/resources/OrganizationSettings.ts +++ b/packages/clerk-js/src/core/resources/OrganizationSettings.ts @@ -23,9 +23,7 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe public constructor(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null = null) { super(); - if (data) { this.fromJSON(data); - } } protected fromJSON(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null): this { diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index b74ce577b93..152a5e28233 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -175,9 +175,7 @@ export class UserSettings extends BaseResource implements UserSettingsResource { public constructor(data: UserSettingsJSON | UserSettingsJSONSnapshot | null = null) { super(); - if (data) { - this.fromJSON(data); - } + this.fromJSON(data); } get instanceIsPasswordBased() { @@ -192,9 +190,6 @@ export class UserSettings extends BaseResource implements UserSettingsResource { ); } - /** - * fromJSON now safely returns the instance even if null is provided. - */ protected fromJSON(data: UserSettingsJSON | UserSettingsJSONSnapshot | null): this { if (!data) { return this; From bf8ec013f5684f2226a8f2885de14d32ee719f47 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 18 Mar 2025 14:26:14 -0500 Subject: [PATCH 19/31] format --- packages/clerk-js/src/core/resources/OrganizationSettings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationSettings.ts b/packages/clerk-js/src/core/resources/OrganizationSettings.ts index 49504ba5279..ea49f111865 100644 --- a/packages/clerk-js/src/core/resources/OrganizationSettings.ts +++ b/packages/clerk-js/src/core/resources/OrganizationSettings.ts @@ -23,7 +23,7 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe public constructor(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null = null) { super(); - this.fromJSON(data); + this.fromJSON(data); } protected fromJSON(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null): this { From 39f1d5c6fee4058dc03eb5756ec7d88e5ac43ad1 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 18 Mar 2025 14:30:30 -0500 Subject: [PATCH 20/31] fix test --- packages/shared/src/__tests__/loadClerkJsScript.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/__tests__/loadClerkJsScript.test.ts b/packages/shared/src/__tests__/loadClerkJsScript.test.ts index 2e259e10292..859de8e479c 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.test.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.test.ts @@ -60,7 +60,7 @@ describe('loadClerkJsScript(options)', () => { const loadPromise = loadClerkJsScript({ publishableKey: mockPublishableKey }); mockExistingScript.dispatchEvent(new Event('error')); - await expect(loadPromise).rejects.toBe('Clerk: Failed to load Clerk'); + await expect(loadPromise).rejects.toThrow('Clerk: Failed to load Clerk'); expect(loadScript).not.toHaveBeenCalled(); }); From d58c2da9601d7ba4a4bb78e4019c7dc5e551cf04 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 19 Mar 2025 11:57:32 -0500 Subject: [PATCH 21/31] wip --- packages/react/src/isomorphicClerk.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index adb6d5ac442..324d95b5e3c 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -458,6 +458,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.eventEmitter.off('loaded', cb); } + /** + * Emits the loaded event. + * This is used to notify listeners that the Clerk instance has been loaded. + * This is only here for testing purposes. + */ + public emitLoaded = () => { + this.eventEmitter.emit('loaded'); + }; + private hydrateClerkJS = (clerkjs: BrowserClerk | HeadlessBrowserClerk | undefined) => { if (!clerkjs) { throw new Error('Failed to hydrate latest Clerk JS'); From 9233a139223edeeb24c1e811270b072ae69b5100 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 19 Mar 2025 13:43:26 -0500 Subject: [PATCH 22/31] move EventEmitter to shared --- packages/react/src/isomorphicClerk.ts | 2 +- packages/shared/package.json | 39 +++++---- .../src}/__tests__/EventEmitter.test.ts | 2 +- packages/shared/src/event-emitter.ts | 86 +++++++++++++++++++ 4 files changed, 108 insertions(+), 21 deletions(-) rename packages/{react/src/utils => shared/src}/__tests__/EventEmitter.test.ts (99%) create mode 100644 packages/shared/src/event-emitter.ts diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 324d95b5e3c..e55b235dcb2 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -1,3 +1,4 @@ +import { EventEmitter } from '@clerk/shared/event-emitter'; import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import { handleValueOrFn } from '@clerk/shared/utils'; import type { @@ -53,7 +54,6 @@ import type { IsomorphicClerkOptions, } from './types'; import { isConstructor } from './utils'; -import { EventEmitter } from './utils/EventEmitter'; if (typeof globalThis.__BUILD_DISABLE_RHC__ === 'undefined') { globalThis.__BUILD_DISABLE_RHC__ = false; diff --git a/packages/shared/package.json b/packages/shared/package.json index 5bb0f04402b..4867de0bdb0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -75,19 +75,22 @@ "main": "./dist/index.js", "files": [ "dist", - "scripts", - "authorization", + "apiUrlFromPublishableKey", "authorization-errors", + "authorization", "browser", - "retry", "color", + "constants", "cookie", "date", "deprecated", "deriveState", + "devBrowser", "dom", "error", + "event-emitter", "file", + "getEnvVariable", "globs", "handleValueOrFn", "isomorphicAtob", @@ -96,28 +99,26 @@ "loadClerkJsScript", "loadScript", "localStorageBroadcastChannel", + "logger", + "oauth", + "object", + "organization", + "pathMatcher", + "pathToRegexp", "poller", "proxy", - "underscore", - "url", - "versionSelector", "react", - "constants", - "apiUrlFromPublishableKey", - "telemetry", - "logger", - "webauthn", + "retry", "router", - "pathToRegexp", + "scripts", + "telemetry", + "underscore", + "url", "utils", - "workerTimers", - "devBrowser", - "object", - "oauth", + "versionSelector", "web3", - "getEnvVariable", - "pathMatcher", - "organization" + "webauthn", + "workerTimers" ], "scripts": { "build": "tsup", diff --git a/packages/react/src/utils/__tests__/EventEmitter.test.ts b/packages/shared/src/__tests__/EventEmitter.test.ts similarity index 99% rename from packages/react/src/utils/__tests__/EventEmitter.test.ts rename to packages/shared/src/__tests__/EventEmitter.test.ts index 2433d80d472..8c334f832b7 100644 --- a/packages/react/src/utils/__tests__/EventEmitter.test.ts +++ b/packages/shared/src/__tests__/EventEmitter.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { EventEmitter } from '../EventEmitter'; +import { EventEmitter } from '../event-emitter'; describe('EventEmitter', () => { let emitter: EventEmitter; diff --git a/packages/shared/src/event-emitter.ts b/packages/shared/src/event-emitter.ts new file mode 100644 index 00000000000..080afdd9f29 --- /dev/null +++ b/packages/shared/src/event-emitter.ts @@ -0,0 +1,86 @@ +/** + * A simple event emitter class for managing event-driven behavior. + */ +export class EventEmitter { + /** + * Stores event listeners, mapping event names to sets of callback functions. + */ + private events: Map void>> = new Map(); + + /** + * Registers a new listener for a specific event. + * + * @param event - The name of the event to listen for. + * @param listener - The callback function to execute when the event is emitted. + */ + on(event: string, listener: (...args: any[]) => void): void { + let listeners = this.events.get(event); + if (!listeners) { + listeners = new Set(); + this.events.set(event, listeners); + } + listeners.add(listener); + } + + /** + * Registers a one-time listener for a specific event. + * The listener is automatically removed after being called once. + * + * @param event - The name of the event to listen for. + * @param listener - The callback function to execute when the event is emitted. + */ + once(event: string, listener: (...args: any[]) => void): void { + const onceWrapper = (...args: any[]) => { + this.off(event, onceWrapper); // Remove after first execution + listener(...args); + }; + + // Store a reference mapping for proper removal + Object.defineProperty(listener, '__onceWrapper', { value: onceWrapper }); + + this.on(event, onceWrapper); + } + + /** + * Removes a specific listener from an event. + * + * @param event - The name of the event. + * @param listener - The callback function to remove. + */ + off(event: string, listener: (...args: any[]) => void): void { + const listeners = this.events.get(event); + if (listeners) { + const wrappedListener = (listener as any).__onceWrapper || listener; + listeners.delete(wrappedListener); + if (listeners.size === 0) { + this.events.delete(event); + } + } + } + + /** + * Emits an event, calling all registered listeners for that event. + * + * @param event - The name of the event to emit. + * @param args - Optional arguments to pass to the event listeners. + */ + emit(event: string, ...args: any[]): void { + const listeners = this.events.get(event); + if (listeners) { + listeners.forEach(listener => listener(...args)); + } + } + + /** + * Removes all listeners for a specific event or clears all events if no event name is provided. + * + * @param event - The name of the event to clear. If omitted, all events will be cleared. + */ + clear(event?: string): void { + if (event) { + this.events.delete(event); + } else { + this.events.clear(); + } + } +} From 7ec756fd765561803ba536d7b75a5baee8fbe5f3 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 19 Mar 2025 20:42:00 -0500 Subject: [PATCH 23/31] wip --- packages/clerk-js/src/core/resources/Base.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Base.ts b/packages/clerk-js/src/core/resources/Base.ts index bc7af640a6a..0e35e23bd86 100644 --- a/packages/clerk-js/src/core/resources/Base.ts +++ b/packages/clerk-js/src/core/resources/Base.ts @@ -77,7 +77,7 @@ export abstract class BaseResource { clerkMissingFapiClientInResources(); } - let fapiResponse: FapiResponse | undefined; + let fapiResponse: FapiResponse; const { fetchMaxTries } = opts; try { @@ -92,15 +92,10 @@ export abstract class BaseResource { console.warn(e); return null; } else { - console.error(e); + throw e; } } - if (typeof fapiResponse === 'undefined') { - // handle offline case - return null; - } - const { payload, status, statusText, headers } = fapiResponse; if (headers) { From 3c6ca76ac5b9c1ddc94d775d28780c3caa36ba4c Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 19 Mar 2025 20:43:48 -0500 Subject: [PATCH 24/31] remove dupe --- packages/react/src/utils/EventEmitter.ts | 86 ------------------------ 1 file changed, 86 deletions(-) delete mode 100644 packages/react/src/utils/EventEmitter.ts diff --git a/packages/react/src/utils/EventEmitter.ts b/packages/react/src/utils/EventEmitter.ts deleted file mode 100644 index 080afdd9f29..00000000000 --- a/packages/react/src/utils/EventEmitter.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * A simple event emitter class for managing event-driven behavior. - */ -export class EventEmitter { - /** - * Stores event listeners, mapping event names to sets of callback functions. - */ - private events: Map void>> = new Map(); - - /** - * Registers a new listener for a specific event. - * - * @param event - The name of the event to listen for. - * @param listener - The callback function to execute when the event is emitted. - */ - on(event: string, listener: (...args: any[]) => void): void { - let listeners = this.events.get(event); - if (!listeners) { - listeners = new Set(); - this.events.set(event, listeners); - } - listeners.add(listener); - } - - /** - * Registers a one-time listener for a specific event. - * The listener is automatically removed after being called once. - * - * @param event - The name of the event to listen for. - * @param listener - The callback function to execute when the event is emitted. - */ - once(event: string, listener: (...args: any[]) => void): void { - const onceWrapper = (...args: any[]) => { - this.off(event, onceWrapper); // Remove after first execution - listener(...args); - }; - - // Store a reference mapping for proper removal - Object.defineProperty(listener, '__onceWrapper', { value: onceWrapper }); - - this.on(event, onceWrapper); - } - - /** - * Removes a specific listener from an event. - * - * @param event - The name of the event. - * @param listener - The callback function to remove. - */ - off(event: string, listener: (...args: any[]) => void): void { - const listeners = this.events.get(event); - if (listeners) { - const wrappedListener = (listener as any).__onceWrapper || listener; - listeners.delete(wrappedListener); - if (listeners.size === 0) { - this.events.delete(event); - } - } - } - - /** - * Emits an event, calling all registered listeners for that event. - * - * @param event - The name of the event to emit. - * @param args - Optional arguments to pass to the event listeners. - */ - emit(event: string, ...args: any[]): void { - const listeners = this.events.get(event); - if (listeners) { - listeners.forEach(listener => listener(...args)); - } - } - - /** - * Removes all listeners for a specific event or clears all events if no event name is provided. - * - * @param event - The name of the event to clear. If omitted, all events will be cleared. - */ - clear(event?: string): void { - if (event) { - this.events.delete(event); - } else { - this.events.clear(); - } - } -} From 3761761d7c6aea1e64cd0f74ea41c75466717d06 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 20 Mar 2025 11:06:35 -0500 Subject: [PATCH 25/31] wip --- packages/clerk-js/src/core/clerk.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ed33d919e98..69db84d889d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,6 +1,6 @@ -import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, EmailLinkErrorCodeStatus, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; +import { EventEmitter } from '@clerk/shared/event-emitter'; import { parsePublishableKey } from '@clerk/shared/keys'; import { LocalStorageBroadcastChannel } from '@clerk/shared/localStorageBroadcastChannel'; import { logger } from '@clerk/shared/logger'; @@ -183,6 +183,7 @@ export class Clerk implements ClerkInterface { protected internal_last_error: ClerkAPIError | null = null; // converted to protected environment to support `updateEnvironment` type assertion protected environment?: EnvironmentResource | null; + private eventEmitter = new EventEmitter(); #publishableKey = ''; #domain: DomainOrProxyUrl['domain']; @@ -257,6 +258,16 @@ export class Clerk implements ClerkInterface { return this.#status; } + set status(status: ClerkInterface['status']) { + if (this.#status === 'ready') { + throw new Error('Clerk status cannot be changed once the instance has been loaded.'); + } + if (status === 'ready') { + this.eventEmitter.emit('ready'); + } + this.#status = status; + } + get isSatellite(): boolean { if (inBrowser()) { return handleValueOrFn(this.#options.isSatellite, new URL(window.location.href), false); From adc0125d4e1df61149077950f65b2d46e6cc0b36 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 20 Mar 2025 20:40:35 -0500 Subject: [PATCH 26/31] add back import --- packages/clerk-js/src/core/clerk.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 69db84d889d..acb0da2f0f7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,3 +1,4 @@ +import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, EmailLinkErrorCodeStatus, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { EventEmitter } from '@clerk/shared/event-emitter'; From 72a09b32b3b40a723c1c349ac87f042bbb677594 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 20 Mar 2025 20:51:41 -0500 Subject: [PATCH 27/31] no vitest in shared :( --- ...{EventEmitter.test.ts => event-emitter.ts} | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) rename packages/shared/src/__tests__/{EventEmitter.test.ts => event-emitter.ts} (87%) diff --git a/packages/shared/src/__tests__/EventEmitter.test.ts b/packages/shared/src/__tests__/event-emitter.ts similarity index 87% rename from packages/shared/src/__tests__/EventEmitter.test.ts rename to packages/shared/src/__tests__/event-emitter.ts index 8c334f832b7..bedf533a44c 100644 --- a/packages/shared/src/__tests__/EventEmitter.test.ts +++ b/packages/shared/src/__tests__/event-emitter.ts @@ -1,5 +1,3 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - import { EventEmitter } from '../event-emitter'; describe('EventEmitter', () => { @@ -10,7 +8,7 @@ describe('EventEmitter', () => { }); it('calls event listeners when an event is emitted', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.on('testEvent', callback); emitter.emit('testEvent'); @@ -18,7 +16,7 @@ describe('EventEmitter', () => { }); it('passes arguments to event listeners', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.on('dataEvent', callback); emitter.emit('dataEvent', 'hello', 42); @@ -26,8 +24,8 @@ describe('EventEmitter', () => { }); it('supports multiple listeners for the same event', () => { - const cb1 = vi.fn(); - const cb2 = vi.fn(); + const cb1 = jest.fn(); + const cb2 = jest.fn(); emitter.on('multiEvent', cb1); emitter.on('multiEvent', cb2); @@ -38,7 +36,7 @@ describe('EventEmitter', () => { }); it('removes a specific listener and prevents it from being called', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.on('removeEvent', callback); emitter.off('removeEvent', callback); emitter.emit('removeEvent'); @@ -47,8 +45,8 @@ describe('EventEmitter', () => { }); it('removes all listeners for a given event', () => { - const cb1 = vi.fn(); - const cb2 = vi.fn(); + const cb1 = jest.fn(); + const cb2 = jest.fn(); emitter.on('clearEvent', cb1); emitter.on('clearEvent', cb2); @@ -60,8 +58,8 @@ describe('EventEmitter', () => { }); it('clears all listeners when no event is specified', () => { - const cb1 = vi.fn(); - const cb2 = vi.fn(); + const cb1 = jest.fn(); + const cb2 = jest.fn(); emitter.on('event1', cb1); emitter.on('event2', cb2); @@ -74,7 +72,7 @@ describe('EventEmitter', () => { }); it('ignores removal of a non-existent listener', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.off('nonExistentEvent', callback); emitter.emit('nonExistentEvent'); @@ -86,7 +84,7 @@ describe('EventEmitter', () => { }); it('removes a listener while executing the event without affecting execution', () => { - const callback = vi.fn(() => { + const callback = jest.fn(() => { emitter.off('selfRemoveEvent', callback); }); @@ -98,8 +96,8 @@ describe('EventEmitter', () => { }); it('allows multiple removals of the same listener without breaking', () => { - const cb1 = vi.fn(); - const cb2 = vi.fn(); + const cb1 = jest.fn(); + const cb2 = jest.fn(); emitter.on('doubleRemoveEvent', cb1); emitter.on('doubleRemoveEvent', cb2); @@ -114,7 +112,7 @@ describe('EventEmitter', () => { describe('once', () => { it('executes a one-time listener only once', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.once('onceEvent', callback); emitter.emit('onceEvent'); @@ -124,7 +122,7 @@ describe('EventEmitter', () => { }); it('passes arguments to a one-time listener', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.once('onceArgsEvent', callback); emitter.emit('onceArgsEvent', 'hello', 42); @@ -133,7 +131,7 @@ describe('EventEmitter', () => { }); it('removes a one-time listener automatically after execution', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.once('autoRemoveEvent', callback); emitter.emit('autoRemoveEvent'); @@ -145,8 +143,8 @@ describe('EventEmitter', () => { }); it('does not affect other listeners on the same event', () => { - const onceCallback = vi.fn(); - const regularCallback = vi.fn(); + const onceCallback = jest.fn(); + const regularCallback = jest.fn(); emitter.once('mixedEvent', onceCallback); emitter.on('mixedEvent', regularCallback); @@ -159,7 +157,7 @@ describe('EventEmitter', () => { }); it('does not call the one-time listener if removed before execution', () => { - const callback = vi.fn(); + const callback = jest.fn(); emitter.once('removeBeforeEvent', callback); emitter.off('removeBeforeEvent', callback); From 474c2ef59c1ec60f1498bb541e1a489601902d40 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 24 Mar 2025 17:44:40 -0500 Subject: [PATCH 28/31] switch uninitialized to loading --- packages/clerk-js/src/core/clerk.ts | 2 +- packages/types/src/clerk.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index cda1ce02510..6f263b6c35d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -199,7 +199,7 @@ export class Clerk implements ClerkInterface { //@ts-expect-error with being undefined even though it's not possible - related to issue with ts and error thrower #fapiClient: FapiClient; #instanceType?: InstanceType; - #status: ClerkInterface['status'] = 'uninitialized'; + #status: ClerkInterface['status'] = 'loading'; #listeners: Array<(emission: Resources) => void> = []; #navigationListeners: Array<() => void> = []; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index e591c020605..4ba2601a157 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -63,7 +63,7 @@ export type SDKMetadata = { environment?: string; }; -export type Status = 'degraded' | 'error' | 'loading' | 'ready' | 'uninitialized'; +export type Status = 'degraded' | 'error' | 'loading' | 'ready'; export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; From c268737923e3c1de957eb33b96e0c349663ece6c Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 26 Mar 2025 16:02:25 -0500 Subject: [PATCH 29/31] clean up status setting --- packages/clerk-js/src/core/clerk.ts | 11 ++++++----- packages/types/src/clerk.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 80e9e977798..26000705c3f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -200,7 +200,7 @@ export class Clerk implements ClerkInterface { //@ts-expect-error with being undefined even though it's not possible - related to issue with ts and error thrower #fapiClient: FapiClient; #instanceType?: InstanceType; - #status: ClerkInterface['status'] = 'loading'; + #status: ClerkInterface['status'] = 'uninitialized'; #listeners: Array<(emission: Resources) => void> = []; #navigationListeners: Array<() => void> = []; @@ -248,7 +248,7 @@ export class Clerk implements ClerkInterface { } get loaded(): boolean { - return this.#status === 'ready'; + return this.status === 'ready'; } get status() { @@ -256,6 +256,7 @@ export class Clerk implements ClerkInterface { } set status(status: ClerkInterface['status']) { + console.log(`status: ${this.#status} -> ${status}`); if (this.#status === 'ready') { throw new Error('Clerk status cannot be changed once the instance has been loaded.'); } @@ -382,7 +383,7 @@ export class Clerk implements ClerkInterface { return; } - this.#status = 'loading'; + this.status = 'loading'; try { if (this.#instanceType === 'development') { @@ -411,9 +412,9 @@ export class Clerk implements ClerkInterface { if (loaded === false) throw new Error('Clerk failed to load'); - this.#status = 'ready'; + this.status = 'ready'; } catch (error) { - this.#status = 'error'; + this.status = 'error'; throw error; } } diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index af3960be296..9215ece83ec 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -63,7 +63,7 @@ export type SDKMetadata = { environment?: string; }; -export type Status = 'degraded' | 'error' | 'loading' | 'ready'; +export type Status = 'degraded' | 'error' | 'loading' | 'ready' | 'uninitialized'; export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; From 76d0d9bf8ee28fc7e011838e18f547c0096c82b4 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 26 Mar 2025 16:33:21 -0500 Subject: [PATCH 30/31] wip --- packages/clerk-js/src/core/clerk.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 26000705c3f..45334db5704 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -256,7 +256,7 @@ export class Clerk implements ClerkInterface { } set status(status: ClerkInterface['status']) { - console.log(`status: ${this.#status} -> ${status}`); + // console.log(`status: ${this.#status} -> ${status}`); if (this.#status === 'ready') { throw new Error('Clerk status cannot be changed once the instance has been loaded.'); } @@ -413,9 +413,8 @@ export class Clerk implements ClerkInterface { if (loaded === false) throw new Error('Clerk failed to load'); this.status = 'ready'; - } catch (error) { + } catch { this.status = 'error'; - throw error; } } From 3edd50f732fca020762d7988b1cfc6c188bd60e9 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 27 Mar 2025 08:39:05 -0500 Subject: [PATCH 31/31] wip --- packages/clerk-js/bundlewatch.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 1cbfaca43fe..cc8c2aa84c8 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "581.5kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "79.30kB" }, + { "path": "./dist/clerk.js", "maxSize": "582kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "80kB" }, { "path": "./dist/clerk.headless.js", "maxSize": "55KB" }, { "path": "./dist/ui-common*.js", "maxSize": "96KB" }, { "path": "./dist/vendors*.js", "maxSize": "30KB" },