import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus'; import { ClerkOfflineError, ClerkRuntimeError, EmailLinkError, EmailLinkErrorCodeStatus, is4xxError, isClerkAPIResponseError, isClerkRuntimeError, isUnauthenticatedError, } from '@clerk/shared/error'; import { disabledAllAPIKeysFeatures, disabledAllBillingFeatures, disabledEmailAddressAttribute, disabledOrganizationAPIKeysFeature, disabledOrganizationsFeature, disabledSelfServeDirectorySyncFeature, disabledSelfServeSSOFeature, disabledUserAPIKeysFeature, isSignedInAndSingleSessionModeEnabled, noOrganizationExists, noUserExists, } from '@clerk/shared/internal/clerk-js/componentGuards'; import { CLERK_SATELLITE_URL, CLERK_SUFFIXED_COOKIES, CLERK_SYNCED, CLERK_SYNCED_STATUS, ERROR_CODES, } from '@clerk/shared/internal/clerk-js/constants'; import { hasMultipleEnterpriseConnections } from '@clerk/shared/internal/clerk-js/enterpriseSSOFactors'; import { RedirectUrls } from '@clerk/shared/internal/clerk-js/redirectUrls'; import { getTaskEndpoint, navigateIfTaskExists, warnMissingPendingTaskHandlers, } from '@clerk/shared/internal/clerk-js/sessionTasks'; import { isValidUrl } from '@clerk/shared/internal/clerk-js/url'; import { warnings } from '@clerk/shared/internal/clerk-js/warnings'; import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate'; import { parsePublishableKey } from '@clerk/shared/keys'; import { logger } from '@clerk/shared/logger'; import { CLERK_NETLIFY_CACHE_BUST_PARAM } from '@clerk/shared/netlifyCacheHandler'; import { AUTO_PROXY_PATH, isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL, shouldAutoProxy, } from '@clerk/shared/proxy'; import { eventPrebuiltComponentMounted, eventPrebuiltComponentOpened, eventThemeUsage, TelemetryCollector, } from '@clerk/shared/telemetry'; import type { __experimental_CheckoutOptions, __internal_AttemptToEnableEnvironmentSettingParams, __internal_AttemptToEnableEnvironmentSettingResult, __internal_CheckoutProps, __internal_EnableOrganizationsPromptProps, __internal_OAuthConsentProps, __internal_PlanDetailsProps, __internal_ProtectCheckModalProps, __internal_SubscriptionDetailsProps, __internal_UserVerificationModalProps, APIKeysNamespace, APIKeysProps, AuthenticateWithBaseParams, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, AuthenticateWithSolanaParams, BillingNamespace, CheckoutSignalValue, Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, ClerkOptions, ClientJSONSnapshot, ClientResource, ConfigureSSOProps, CreateOrganizationParams, CreateOrganizationProps, CredentialReturn, DomainOrProxyUrl, EnvironmentJSON, EnvironmentJSONSnapshot, EnvironmentResource, GenerateSignature, GoogleOneTapProps, HandleEmailLinkVerificationParams, HandleOAuthCallbackParams, InstanceType, InviteMembersModalProps, JoinWaitlistParams, ListenerCallback, ListenerOptions, LoadedClerk, NavigateOptions, OAuthApplicationNamespace, OAuthDeviceVerificationProps, OAuthTransport, OrganizationListProps, OrganizationProfileProps, OrganizationResource, OrganizationSwitcherProps, PricingTableProps, ProtectAssertion, ProtectCheckFlow, PublicKeyCredentialCreationOptionsWithoutExtensions, PublicKeyCredentialRequestOptionsWithoutExtensions, PublicKeyCredentialWithAuthenticatorAssertionResponse, PublicKeyCredentialWithAuthenticatorAttestationResponse, RedirectOptions, Resources, ResumeAfterProtectCheckParams, SDKMetadata, SessionResource, SessionTouchParams, SetActiveParams, SignedInSessionResource, SignInProps, SignInRedirectOptions, SignInResource, SignOut, SignOutCallback, SignOutOptions, SignUpProps, SignUpRedirectOptions, SignUpResource, TaskChooseOrganizationProps, TaskResetPasswordProps, TaskSetupMFAProps, TasksRedirectOptions, UnsubscribeCallback, UserAvatarProps, UserButtonProps, UserProfileProps, UserResource, WaitlistProps, WaitlistResource, Web3Provider, } from '@clerk/shared/types'; import type { ClerkUI } from '@clerk/shared/ui'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { allSettled, handleValueOrFn, noop, timeLimit } from '@clerk/shared/utils'; import type { QueryClient } from '@tanstack/query-core'; import { debugLogger, initDebugLogger } from '@/utils/debug'; import { ModuleManager } from '@/utils/moduleManager'; import { ALLOWED_PROTOCOLS, buildURL, createAllowedRedirectOrigins, createBeforeUnloadTracker, createPageLifecycle, errorThrower, getClerkQueryParam, hasExternalAccountSignUpError, inActiveBrowserTab, inBrowser, isDevAccountPortalOrigin, isError, isOrganizationId, isRedirectForFAPIInitiatedFlow, navigateToNextStepSignUp, removeClerkQueryParam, requiresUserInput, stripOrigin, web3, } from '../utils'; import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../utils/localStorage'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; import { AuthCookieService } from './auth/AuthCookieService'; import { CaptchaHeartbeat } from './auth/CaptchaHeartbeat'; import { clerkErrorInitFailed, clerkInvalidSignInUrlFormat, clerkInvalidSignInUrlOrigin, clerkMissingProxyUrlAndDomain, clerkMissingSignInUrlAsSatellite, clerkOAuthCallbackDidNotCompleteSignInSignUp, clerkRedirectUrlIsMissingScheme, clerkUnsupportedEnvironmentWarning, } from './errors'; import { eventBus, events } from './events'; import type { FapiClient, FapiRequestCallback } from './fapiClient'; import { createFapiClient } from './fapiClient'; import { createClientFromJwt } from './jwt-client'; import { APIKeys } from './modules/apiKeys'; import { Billing } from './modules/billing'; import { createCheckoutInstance } from './modules/checkout/instance'; import { OAuthApplication } from './modules/oauthApplication'; import { Protect } from './protect'; import { protectAssertionParams } from './protectAssertion'; import { ProtectCheckGate } from './protectCheckGate'; import type { SignIn, SignUp } from './resources/internal'; import { BaseResource, Client, Environment, Organization, Waitlist } from './resources/internal'; import { State } from './state'; type SetActiveHook = (intent?: 'sign-out') => void | Promise; declare global { interface Window { Clerk?: Clerk; __clerk_publishable_key?: string; __clerk_proxy_url?: ClerkInterface['proxyUrl']; __clerk_domain?: ClerkInterface['domain']; } } const CANNOT_RENDER_BILLING_DISABLED_ERROR_CODE = 'cannot_render_billing_disabled'; const CANNOT_RENDER_USER_MISSING_ERROR_CODE = 'cannot_render_user_missing'; const CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE = 'cannot_render_organizations_disabled'; const CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE = 'cannot_render_organization_missing'; const CANNOT_RENDER_PERMISSION_MISSING_ERROR_CODE = 'cannot_render_permission_missing'; const CANNOT_RENDER_SINGLE_SESSION_ENABLED_ERROR_CODE = 'cannot_render_single_session_enabled'; const CANNOT_RENDER_API_KEYS_DISABLED_ERROR_CODE = 'cannot_render_api_keys_disabled'; const CANNOT_RENDER_API_KEYS_USER_DISABLED_ERROR_CODE = 'cannot_render_api_keys_user_disabled'; const CANNOT_RENDER_API_KEYS_ORG_DISABLED_ERROR_CODE = 'cannot_render_api_keys_org_disabled'; const CANNOT_RENDER_SELF_SERVE_SSO_DISABLED_ERROR_CODE = 'cannot_render_self_serve_sso_disabled'; const CANNOT_RENDER_CONFIGURE_SSO_EMAIL_ADDRESS_DISABLED_ERROR_CODE = 'cannot_render_configure_sso_email_address_disabled'; const INITIALIZATION_TIMEOUT_MS = 7_000; const defaultOptions: ClerkOptions = { polling: true, standardBrowser: true, touchSession: true, isSatellite: false, signInUrl: undefined, signUpUrl: undefined, afterSignOutUrl: undefined, signInFallbackRedirectUrl: undefined, signUpFallbackRedirectUrl: undefined, signInForceRedirectUrl: undefined, signUpForceRedirectUrl: undefined, newSubscriptionRedirectUrl: undefined, }; export class Clerk implements ClerkInterface { public static version: string = __PKG_VERSION__; public static sdkMetadata: SDKMetadata = { name: __PKG_NAME__, version: __PKG_VERSION__, }; private static _billing: BillingNamespace; private static _apiKeys: APIKeysNamespace; private static _oauthApplication: OAuthApplicationNamespace; private _checkout: ClerkInterface['__experimental_checkout'] | undefined; public client: ClientResource | undefined; public session: SignedInSessionResource | null | undefined; public organization: OrganizationResource | null | undefined; public user: UserResource | null | undefined; public __internal_country?: string | null; public telemetry: TelemetryCollector | undefined; public readonly __internal_state: State = new State(); protected internal_last_error: ClerkAPIError | null = null; // converted to protected environment to support `updateEnvironment` type assertion protected environment?: EnvironmentResource | null; #queryClient: QueryClient | undefined; #publishableKey = ''; #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; #authService?: AuthCookieService; #protect?: Protect; #captchaHeartbeat?: CaptchaHeartbeat; #broadcastChannel: BroadcastChannel | null = null; #clerkUI?: Promise; //@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'; #initialResourcesReloadedDuringLoad = false; #listeners: Array<(emission: Resources) => void> = []; #navigationListeners: Array<() => void> = []; #options: ClerkOptions = {}; #protectAssertion: ProtectAssertion | undefined; // Distinguishes never-set from cleared-with-undefined, so clearing does not fall back to the option. #protectAssertionSet = false; #oauthTransport: OAuthTransport | null = null; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; #publicEventBus = createClerkEventBus(); #moduleManager = new ModuleManager(); /** * Cross-bundle handle to the ModuleManager. clerk-js is loaded standalone * from the CDN with its own inlined @clerk/shared, so plain property access * is the only channel that reliably crosses that boundary. This getter is * how IsomorphicClerk forwards the manager to consumers that import * @clerk/shared from node_modules (e.g. @clerk/react, @clerk/ui). * * @internal */ get __internal_moduleManager(): ModuleManager { return this.#moduleManager; } get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined { if (!this.#queryClient) { void import('./query-core') .then(module => module.QueryClient) .then(QueryClient => { if (this.#queryClient) { return; } this.#queryClient = new QueryClient(); // @ts-expect-error - queryClientStatus is not typed this.#publicEventBus.emit('queryClientStatus', 'ready'); }); } return this.#queryClient ? { __tag: 'clerk-rq-client', client: this.#queryClient, } : undefined; } get __internal_hasOAuthTransport(): boolean { return this.#oauthTransport !== null; } get __internal_oauthTransport(): OAuthTransport | null { return this.#oauthTransport; } /** * The verification-module load timeout asked for by the loader THIS browser was assigned, or * undefined when it asked for nothing. Exposed because the assignment is a random draw per page * load and cannot be recomputed from the environment config; callers fall back to the * instance-wide value on that config, and then to the SDK default. */ get __internal_protectChallengeLoadTimeoutMs(): number | undefined { return this.#protect?.challengeLoadTimeoutMs; } public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) | undefined; public __internal_createPublicCredentials: | (( publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions, ) => Promise>) | undefined; public __internal_getPublicCredentials: | (({ publicKeyOptions, }: { publicKeyOptions: PublicKeyCredentialRequestOptionsWithoutExtensions; }) => Promise>) | undefined; public __internal_isWebAuthnSupported: (() => boolean) | undefined; public __internal_isWebAuthnAutofillSupported: (() => Promise) | undefined; public __internal_isWebAuthnPlatformAuthenticatorSupported: (() => Promise) | undefined; public __internal_setActiveInProgress = false; #redirectCallbackInFlight: Promise | undefined; get publishableKey(): string { return this.#publishableKey; } get version(): string { return Clerk.version; } get uiVersion(): string | undefined { // `@clerk/ui` publishes its constructor (which carries its package version) on this global when hot-loaded // from the CDN; bundled (no-RHC) builds pass the constructor directly via `options.ui.ClerkUI` instead. const globalCtor = typeof window !== 'undefined' ? window.__internal_ClerkUICtor : undefined; const bundledCtor = this.#options.ui?.ClerkUI; return globalCtor?.version ?? (bundledCtor instanceof Promise ? undefined : bundledCtor?.version); } set sdkMetadata(metadata: SDKMetadata) { Clerk.sdkMetadata = metadata; } get sdkMetadata(): SDKMetadata { return Clerk.sdkMetadata; } get loaded(): boolean { return this.status === 'degraded' || this.status === 'ready'; } get status(): ClerkInterface['status'] { return this.#status; } get isSatellite(): boolean { if (inBrowser()) { return handleValueOrFn(this.#options.isSatellite, new URL(window.location.href), false); } return false; } get domain(): string { if (inBrowser()) { const strippedDomainString = stripScheme(handleValueOrFn(this.#domain, new URL(window.location.href))); if (this.#instanceType === 'production') { return addClerkPrefix(strippedDomainString); } return strippedDomainString; } if (typeof this.#domain === 'function') { logger.warnOnce(warnings.domainAsFunctionNotSupported); return ''; } return stripScheme(this.#domain || ''); } get proxyUrl(): string { if (inBrowser()) { const _unfilteredProxy = handleValueOrFn(this.#proxyUrl, new URL(window.location.href)); if (!isValidProxyUrl(_unfilteredProxy)) { errorThrower.throwInvalidProxyUrl({ url: _unfilteredProxy }); } const resolved = proxyUrlToAbsoluteURL(_unfilteredProxy); if (resolved) { return resolved; } // Auto-detect when no explicit proxy or domain is configured (production only) if (!this.#domain && this.#instanceType === 'production' && shouldAutoProxy(window.location.hostname)) { return `${window.location.origin}${AUTO_PROXY_PATH}`; } } if (typeof this.#proxyUrl === 'function') { logger.warnOnce(warnings.proxyUrlAsFunctionNotSupported); return ''; } return this.#proxyUrl || ''; } get frontendApi(): string { const publishableKey = parsePublishableKey(this.publishableKey); if (!publishableKey) { return errorThrower.throwInvalidPublishableKeyError({ key: this.publishableKey, }); } return publishableKey.frontendApi; } get instanceType() { return this.#instanceType; } get isStandardBrowser(): boolean { return this.#options.standardBrowser || false; } get billing(): BillingNamespace { if (!Clerk._billing) { Clerk._billing = new Billing(); } return Clerk._billing; } get apiKeys(): APIKeysNamespace { if (!Clerk._apiKeys) { Clerk._apiKeys = new APIKeys(); } return Clerk._apiKeys; } get oauthApplication(): OAuthApplicationNamespace { if (!Clerk._oauthApplication) { Clerk._oauthApplication = new OAuthApplication(); } return Clerk._oauthApplication; } __experimental_checkout(options: __experimental_CheckoutOptions): CheckoutSignalValue { if (!this._checkout) { this._checkout = (params: any) => createCheckoutInstance(this, params); } return this._checkout(options); } public __internal_getOption(key: K): ClerkOptions[K] { return this.#options[key]; } public setProtectAssertion = (assertion?: ProtectAssertion): void => { this.#protectAssertion = assertion; this.#protectAssertionSet = true; }; /** The last value passed to `setProtectAssertion` once called, otherwise the `protectAssertion` option. */ #currentProtectAssertion(): ProtectAssertion | undefined { return this.#protectAssertionSet ? this.#protectAssertion : this.#options.protectAssertion; } /** * The Protect params for one sign-in or sign-up body. The application-supplied assertion and the * server-configured session token are independent features that write disjoint params, so both * contribute and neither can suppress the other: they are resolved concurrently, and a failure on * one side costs only that side's params. Resolves to `undefined` when neither produces anything, * so a request from an instance using no Protect feature is byte-for-byte what it was before. */ async #protectParams(): Promise | undefined> { const [assertion, session] = await Promise.all([ protectAssertionParams(this.#currentProtectAssertion()).catch(() => undefined), this.#protect?.getRequestParams().catch(() => undefined), ]); if (!assertion && !session) { return undefined; } return { ...assertion, ...session }; } get isSignedIn(): boolean { const hasPendingSession = this?.session?.status === 'pending'; if (hasPendingSession) { return false; } return !!this.session; } public constructor(key: string, options?: DomainOrProxyUrl) { key = (key || '').trim(); if (!key) { return errorThrower.throwMissingPublishableKeyError(); } const publishableKey = parsePublishableKey(key); if (!publishableKey) { return errorThrower.throwInvalidPublishableKeyError({ key }); } this.#domain = options?.domain; this.#proxyUrl = options?.proxyUrl; this.environment = Environment.getInstance(); this.#instanceType = publishableKey.instanceType; this.#publishableKey = key; this.#fapiClient = createFapiClient({ domain: this.domain, frontendApi: this.frontendApi, // this.instanceType is assigned above instanceType: this.instanceType as InstanceType, isSatellite: this.isSatellite, getSessionId: () => { return this.session?.id; }, getProtectParams: () => this.#protectParams(), proxyUrl: this.proxyUrl, }); this.#publicEventBus.emit(clerkEvents.Status, 'loading'); this.#publicEventBus.prioritizedOn(clerkEvents.Status, s => (this.#status = s)); this.#publicEventBus.on(clerkEvents.Status, status => { if (!inBrowser()) { return; } if (status === 'ready' || status === 'degraded') { if (window.__clerk_internal_ready?.__resolve && this.#isLoaded()) { window.__clerk_internal_ready.__resolve(this); } } else if (status === 'error') { if (window.__clerk_internal_ready?.__reject) { window.__clerk_internal_ready.__reject( new ClerkRuntimeError('Clerk failed to initialize.', { code: 'clerk_init_failed' }), ); } } }); if (inBrowser() && (this.#status === 'ready' || this.#status === 'degraded') && this.#isLoaded()) { if (window.__clerk_internal_ready?.__resolve) { window.__clerk_internal_ready.__resolve(this); } } // This line is used for the piggy-backing mechanism BaseResource.clerk = this; if (!__BUILD_DISABLE_RHC__) { this.#protect = new Protect(); } } public getFapiClient = (): FapiClient => this.#fapiClient; public load = async (options?: ClerkOptions): Promise => { debugLogger.info('load() start', {}, 'clerk'); if (this.loaded) { return; } // Log a development mode warning once if (this.#instanceType === 'development' && !options?.unsafe_disableDevelopmentModeConsoleWarning) { 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', ); } this.#options = this.#initOptions(options); this.#oauthTransport = this.#options.__internal_oauthTransport ?? null; // Initialize ClerkUI if it was provided if (this.#options.ui?.ClerkUI) { this.#clerkUI = Promise.resolve(this.#options.ui.ClerkUI).then( ClerkUI => new ClerkUI( () => this, () => this.environment, this.#options, this.#moduleManager, ), ); } // In development mode, if custom router options are provided, warn if both routerPush and routerReplace are not provided if ( this.#instanceType === 'development' && (this.#options.routerPush || this.#options.routerReplace) && (!this.#options.routerPush || !this.#options.routerReplace) ) { // Typing this.#options as ClerkOptions to ensure proper type checking. TypeScript will infer the type as `never` // since missing both `routerPush` and `routerReplace` is not a valid ClerkOptions. const options = this.#options as ClerkOptions; const missingRouter = !options.routerPush ? 'routerPush' : 'routerReplace'; logger.warnOnce( `Clerk: Both \`routerPush\` and \`routerReplace\` need to be defined, but \`${missingRouter}\` is not defined. This may cause issues with navigation in your application.`, ); } /** * Listen to `Session.getToken` resolving to emit the updated session * with the new token to the state listeners. */ eventBus.on(events.SessionTokenResolved, () => { this.#updateAccessors(this.session); }); if (this.#options.sdkMetadata) { Clerk.sdkMetadata = this.#options.sdkMetadata; } if (this.#options.telemetry !== false) { this.telemetry = new TelemetryCollector({ clerkVersion: Clerk.version, samplingRate: 1, perEventSampling: this.#options.__internal_keyless_claimKeylessApplicationUrl ? false : undefined, publishableKey: this.publishableKey, ...this.#options.telemetry, }); // Record theme usage telemetry when appearance is provided if (this.#options.appearance) { this.telemetry.record(eventThemeUsage(this.#options.appearance)); } } try { if (this.#options.standardBrowser) { await this.#loadInStandardBrowser(); } else { await this.#loadInNonStandardBrowser(); } this.#finishLoad(); } catch (error) { // A reload that succeeded while this load was running already applied newer resources, so the load finishes. if (!this.#options.standardBrowser && this.#initialResourcesReloadedDuringLoad && this.#status === 'loading') { this.#finishLoad(); this.#publicEventBus.emit(clerkEvents.Status, 'ready'); return; } this.#publicEventBus.emit(clerkEvents.Status, 'error'); debugLogger.error('load() failed', { error }, 'clerk'); // bubble up the error throw error; } }; #finishLoad = (): void => { const telemetry = this.#options.telemetry; const telemetryEnabled = telemetry !== false && !telemetry?.disabled; const isKeyless = Boolean(this.#options.__internal_keyless_claimKeylessApplicationUrl); const hasClientDebugMode = Boolean(this.environment?.clientDebugMode); const isProd = this.environment?.isProduction?.() ?? false; const shouldEnable = hasClientDebugMode || (isKeyless && !isProd); const logLevel = isKeyless && !hasClientDebugMode ? 'error' : undefined; if (shouldEnable) { initDebugLogger({ enabled: true, ...(logLevel ? { logLevel } : {}), ...(telemetryEnabled && this.telemetry ? { telemetryCollector: this.telemetry } : {}), }); } this.#protect?.load(this.environment as Environment, this.frontendApi); debugLogger.info('load() complete', {}, 'clerk'); }; #isCombinedSignInOrUpFlow(): boolean { return Boolean(!this.#options.signUpUrl && this.#options.signInUrl && !isAbsoluteUrl(this.#options.signInUrl)); } /** * Routes `url` through `/v1/client/touch` when the client cookie is close to expiring, * and returns it unchanged otherwise. * * In practice this only adds a redirect on Safari. A cookie re-issued from a fetch is * capped at 7 days by ITP; every other browser gets the full 400 days and is never * eligible. Touch is a top-level navigation, which ITP does not cap, so it restores * the full lifetime. */ #decorateUrlWithTouch(url: string): string { // In React Native, window exists but window.location does not, so the URL cannot be // resolved to an absolute one. ITP does not apply there either. if (!inBrowser() || typeof window.location === 'undefined' || !this.client?.isEligibleForTouch()) { return url; } return this.buildUrlWithAuth(this.client.buildTouchUrl({ redirectUrl: new URL(url, window.location.href) })); } public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { if (!this.client || this.client.sessions.length === 0) { return; } const onBeforeSetActive: SetActiveHook = typeof window !== 'undefined' && typeof window.__internal_onBeforeSetActive === 'function' ? window.__internal_onBeforeSetActive : noop; const onAfterSetActive: SetActiveHook = typeof window !== 'undefined' && typeof window.__internal_onAfterSetActive === 'function' ? window.__internal_onAfterSetActive : noop; const opts = callbackOrOptions && typeof callbackOrOptions === 'object' ? callbackOrOptions : options || {}; const redirectUrl = opts?.redirectUrl || this.buildAfterSignOutUrl(); let debugRedirectUrl: string | undefined; try { debugRedirectUrl = stripOrigin(redirectUrl); } catch { debugRedirectUrl = undefined; } debugLogger.debug( 'signOut() start', { hasClient: Boolean(this.client), multiSessionCount: this.client?.signedInSessions.length ?? 0, redirectUrl: debugRedirectUrl, sessionTarget: opts?.sessionId ?? null, }, 'clerk', ); const signOutCallback = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined; const executeSignOut = async (signOutUrl?: string) => { const tracker = createBeforeUnloadTracker(this.#options.standardBrowser); // Notify other tabs that user is signing out and clean up cookies. eventBus.emit(events.UserSignOut, null); await tracker.track(async () => { if (signOutUrl) { await this.navigate(signOutUrl); } else if (signOutCallback) { await signOutCallback(); } else { // The client outlives sign-out and is what Client Trust uses to recognize a // returning device, so the re-issued cookie has to keep its full lifetime. await this.navigate(this.#decorateUrlWithTouch(redirectUrl)); } }); if (tracker.isUnloading()) { return; } this.#updateAccessors(); await onAfterSetActive(); }; /** * Clears the router cache for `@clerk/nextjs` on all routes except the current one. * Note: Calling `onBeforeSetActive` before signing out, allows for new RSC prefetch requests to render as signed in. * Since we are calling `onBeforeSetActive` before signing out, we should NOT pass `"sign-out"`. */ await onBeforeSetActive(); if (!opts.sessionId || this.client.signedInSessions.length === 1) { this.#setTransitiveState(); const persistClient = this.#options.experimental?.persistClient ?? true; let repairRedirectUrl: URL | undefined; if ( persistClient && (!signOutCallback || !!opts.__internal_satelliteSignOutRedirectUrl) && this.#instanceType === 'production' && this.#options.standardBrowser && inBrowser() && typeof window.location !== 'undefined' && this.isSatellite ) { try { const target = new URL(opts.__internal_satelliteSignOutRedirectUrl || redirectUrl, window.location.href); if (target.protocol === 'https:' && !target.username && !target.password) { repairRedirectUrl = target; } } catch { repairRedirectUrl = undefined; } } let signOutUrl: string | undefined; try { if (repairRedirectUrl && Client.isClientResource(this.client)) { ({ signOutUrl } = await this.client.__internal_signOut({ redirectUrl: repairRedirectUrl.href, })); } else if (persistClient) { await this.client.removeSessions(); } else { await this.client.destroy(); } } catch (err) { if (!isClerkRuntimeError(err) || err.code !== 'network_error') { throw err; } } const repairUrl = isValidUrl(signOutUrl) && signOutUrl?.startsWith('https://') ? signOutUrl : undefined; await executeSignOut(repairUrl); debugLogger.info('signOut() complete', { redirectUrl: debugRedirectUrl }, 'clerk'); return; } // Multi-session handling const session = this.client.signedInSessions.find(s => s.id === opts.sessionId); const shouldSignOutCurrent = session?.id && this.session?.id === session.id; if (shouldSignOutCurrent) { this.#setTransitiveState(); await session?.remove(); await executeSignOut(); debugLogger.info('signOut() complete', { redirectUrl: debugRedirectUrl }, 'clerk'); } else { await session?.remove(); } }; public openGoogleOneTap = (props?: GoogleOneTapProps): void => { this.assertComponentsReady(this.#clerkUI); const component = 'GoogleOneTap'; void this.#clerkUI.then(ui => ui.ensureMounted()).then(controls => controls.openModal('googleOneTap', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened(component, props)); }; public closeGoogleOneTap = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('googleOneTap')); }; public openSignIn = (props?: SignInProps): void => { if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: CANNOT_RENDER_SINGLE_SESSION_ENABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'SignIn'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.openModal('signIn', props || {})); const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() }; this.telemetry?.record(eventPrebuiltComponentOpened(component, props, additionalData)); }; public closeSignIn = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('signIn')); }; public __internal_openCheckout = (props?: __internal_CheckoutProps): void => { if (disabledAllBillingFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyBillingComponent('Checkout'), { code: CANNOT_RENDER_BILLING_DISABLED_ERROR_CODE, }); } return; } if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenCheckout, { code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI.then(ui => ui.ensureMounted()).then(controls => controls.openDrawer('checkout', props || {})); }; public __internal_closeCheckout = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeDrawer('checkout')); }; public __internal_openPlanDetails = (props: __internal_PlanDetailsProps): void => { if (disabledAllBillingFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyBillingComponent('PlanDetails'), { code: CANNOT_RENDER_BILLING_DISABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'PlanDetails'; void this.#clerkUI.then(ui => ui.ensureMounted()).then(controls => controls.openDrawer('planDetails', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened(component, props)); }; public __internal_closePlanDetails = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeDrawer('planDetails')); }; public __internal_openSubscriptionDetails = (props?: __internal_SubscriptionDetailsProps): void => { this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted()) .then(controls => controls.openDrawer('subscriptionDetails', props || {})); }; public __internal_closeSubscriptionDetails = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeDrawer('subscriptionDetails')); }; public __internal_openReverification = (props?: __internal_UserVerificationModalProps): void => { if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, { code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted()) .then(controls => controls.openModal('userVerification', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened(`UserVerification`, props)); }; public __internal_closeReverification = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('userVerification')); }; public __internal_attemptToEnableEnvironmentSetting = ( params: __internal_AttemptToEnableEnvironmentSettingParams, ): __internal_AttemptToEnableEnvironmentSettingResult => { const { for: setting, caller } = params; if (!this.user && this.#instanceType === 'development') { logger.warnOnce( `Clerk: "${caller}" requires an active user session. Ensure a user is signed in before executing ${caller}.`, ); } switch (setting) { case 'organizations': { const isSettingDisabled = disabledOrganizationsFeature(this, this.environment) && // Handles case where environment gets enabled via BAPI, but it gets cached and the user is redirected to the choose-organization task // The enable org prompt should not appear in the task screen since orgs have already been enabled this.session?.currentTask?.key !== 'choose-organization'; if (!isSettingDisabled) { return { isEnabled: true }; } if (this.#instanceType === 'development') { this.__internal_openEnableOrganizationsPrompt({ caller, // Reload current window to all invalidate all resources // related to organizations, eg: roles onSuccess: () => window.location.reload(), onClose: params.onClose, } as __internal_EnableOrganizationsPromptProps); } return { isEnabled: false }; } default: throw new Error(`Attempted to enable an unknown or unsupported setting "${setting}".`); } }; public __internal_openEnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps): void => { this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: 'EnableOrganizationsPrompt' })) .then(controls => controls.openModal('enableOrganizationsPrompt', props || {})); this.telemetry?.record(eventPrebuiltComponentMounted('EnableOrganizationsPrompt', props)); }; public __internal_closeEnableOrganizationsPrompt = (): void => { this.assertComponentsReady(this.#clerkUI); void this.#clerkUI ?.then(ui => ui.ensureMounted()) .then(controls => controls.closeModal('enableOrganizationsPrompt')); }; public __internal_resolvePendingProtectCheck = async (flow?: ProtectCheckFlow): Promise => { const client = this.client; if (!client || client.signIn.status === 'complete' || client.signUp.status === 'complete') { return; } const gate = ProtectCheckGate.getInstance(); const resolve = async (resource: SignInResource | SignUpResource) => { try { await gate.resolve(this, resource); } catch (error) { eventBus.emit(events.ResourceError, { resource: resource as SignIn | SignUp, error }); throw error; } }; if (flow !== 'signUp') { await resolve(client.signIn); } if (flow !== 'signIn') { await resolve(client.signUp); } }; public __internal_openProtectCheckModal = ( props: Pick<__internal_ProtectCheckModalProps, 'resource'>, ): Promise => { if (!this.#clerkUI) { return Promise.resolve(); } return this.#clerkUI .then(ui => ui.ensureMounted()) .then(controls => { if (!controls.openProtectCheckModal) { return; } return new Promise((resolve, reject) => { controls.openProtectCheckModal?.({ ...props, onResolved: () => { controls.closeModal('protectCheck'); resolve(); }, onFailed: error => { controls.closeModal('protectCheck'); reject(error); }, }); }); }); }; public __internal_openBlankCaptchaModal = (): Promise => { this.assertComponentsReady(this.#clerkUI); return this.#clerkUI.then(ui => ui.ensureMounted()).then(controls => controls.openModal('blankCaptcha', {})); }; public __internal_closeBlankCaptchaModal = (): Promise => { this.assertComponentsReady(this.#clerkUI); return this.#clerkUI.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('blankCaptcha')); }; public __internal_loadStripeJs = async () => { if (__BUILD_DISABLE_RHC__) { clerkUnsupportedEnvironmentWarning('Stripe'); return { loadStripe: () => Promise.resolve(null) }; } const { loadStripe } = await import('@stripe/stripe-js'); return loadStripe; }; public openSignUp = (props?: SignUpProps): void => { if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: CANNOT_RENDER_SINGLE_SESSION_ENABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: 'SignUp' })) .then(controls => controls.openModal('signUp', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('SignUp', props)); }; public closeSignUp = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('signUp')); }; public openUserProfile = (props?: UserProfileProps): void => { if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, { code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: 'UserProfile' })) .then(controls => controls.openModal('userProfile', props || {})); const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined; this.telemetry?.record(eventPrebuiltComponentOpened('UserProfile', props, additionalData)); }; public closeUserProfile = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('userProfile')); }; public openOrganizationProfile = (props?: OrganizationProfileProps): void => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationProfile', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } if (noOrganizationExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('OrganizationProfile'), { code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: 'OrganizationProfile' })) .then(controls => controls.openModal('organizationProfile', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('OrganizationProfile', props)); }; public closeOrganizationProfile = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('organizationProfile')); }; public openInviteMembers = (props?: InviteMembersModalProps): void => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'InviteMembers', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('InviteMembers'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } if (noOrganizationExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('InviteMembers'), { code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE, }); } return; } if (!this.session?.checkAuthorization({ permission: 'org:sys_memberships:manage' })) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError( warnings.createCannotRenderComponentWhenPermissionIsMissing('InviteMembers', 'org:sys_memberships:manage'), { code: CANNOT_RENDER_PERMISSION_MISSING_ERROR_CODE }, ); } return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted()) .then(controls => controls.openModal('inviteMembers', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('InviteMembers', props)); }; public closeInviteMembers = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('inviteMembers')); }; public openCreateOrganization = (props?: CreateOrganizationProps): void => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'CreateOrganization', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: 'CreateOrganization' })) .then(controls => controls.openModal('createOrganization', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('CreateOrganization', props)); }; public closeCreateOrganization = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('createOrganization')); }; public openWaitlist = (props?: WaitlistProps): void => { this.assertComponentsReady(this.#clerkUI); void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: 'Waitlist' })) .then(controls => controls.openModal('waitlist', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('Waitlist', props)); }; public closeWaitlist = (): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('waitlist')); }; public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => { this.assertComponentsReady(this.#clerkUI); const component = 'SignIn'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'signIn', node, props, }), ); const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() }; this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData)); }; public unmountSignIn = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountUserAvatar = (node: HTMLDivElement, props?: UserAvatarProps): void => { this.assertComponentsReady(this.#clerkUI); const component = 'UserAvatar'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'userAvatar', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountUserAvatar = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => { this.assertComponentsReady(this.#clerkUI); const component = 'SignUp'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'signUp', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountSignUp = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountUserProfile = (node: HTMLDivElement, props?: UserProfileProps): void => { if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenUserDoesNotExist, { code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'UserProfile'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'userProfile', node, props, }), ); const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined; this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData)); }; public unmountUserProfile = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationProfile', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } const userExists = !noUserExists(this); if (noOrganizationExists(this) && userExists) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('OrganizationProfile'), { code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'OrganizationProfile'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'userProfile', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountOrganizationProfile = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'CreateOrganization', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } this.assertComponentsReady(this.#clerkUI); const component = 'CreateOrganization'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'createOrganization', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountCreateOrganization = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationSwitcher', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } this.assertComponentsReady(this.#clerkUI); const component = 'OrganizationSwitcher'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'organizationSwitcher', node, props, }), ); this.telemetry?.record( eventPrebuiltComponentMounted(component, { ...props, forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection, }), ); }; public unmountOrganizationSwitcher = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public __experimental_prefetchOrganizationSwitcher = () => { this.assertComponentsReady(this.#clerkUI); void this.#clerkUI.then(ui => ui.ensureMounted()).then(controls => controls.prefetch('organizationSwitcher')); }; public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationList', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationList'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } this.assertComponentsReady(this.#clerkUI); const component = 'OrganizationList'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'organizationList', node, props, }), ); this.telemetry?.record( eventPrebuiltComponentMounted(component, { ...props, forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection, }), ); }; public unmountOrganizationList = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountUserButton = (node: HTMLDivElement, props?: UserButtonProps) => { this.assertComponentsReady(this.#clerkUI); const component = 'UserButton'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'userButton', node, props, }), ); const additionalData = { ...(props?.customMenuItems?.length || 0 > 0 ? { customItems: true } : undefined), ...(props?.__experimental_asStandalone ? { standalone: true } : undefined), }; this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData)); }; public unmountUserButton = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountWaitlist = (node: HTMLDivElement, props?: WaitlistProps) => { this.assertComponentsReady(this.#clerkUI); const component = 'Waitlist'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'waitlist', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountWaitlist = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountPricingTable = (node: HTMLDivElement, props?: PricingTableProps): void => { if (disabledAllBillingFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyBillingComponent('PricingTable'), { code: CANNOT_RENDER_BILLING_DISABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'PricingTable'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'pricingTable', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountPricingTable = (node: HTMLDivElement): void => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountOAuthConsent = (node: HTMLDivElement, props?: __internal_OAuthConsentProps) => { if (this.user === null) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderOAuthConsentComponentWhenUserDoesNotExist, { code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'OAuthConsent'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: '__internal_oauthConsent', node, props, }), ); }; public unmountOAuthConsent = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** * @deprecated Use mountOAuthConsent instead. */ public __internal_mountOAuthConsent = (node: HTMLDivElement, props?: __internal_OAuthConsentProps) => { return this.mountOAuthConsent(node, props); }; /** * @deprecated Use unmountOAuthConsent instead. */ public __internal_unmountOAuthConsent = (node: HTMLDivElement) => { return this.unmountOAuthConsent(node); }; public __internal_mountOAuthDeviceVerification = (node: HTMLDivElement, props?: OAuthDeviceVerificationProps) => { if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderOAuthDeviceVerificationComponentWhenUserDoesNotExist, { code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'OAuthDeviceVerification'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'oauthDeviceVerification', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public __internal_unmountOAuthDeviceVerification = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** * Mount an API keys component at the target element. * @param targetNode Target to mount the APIKeys component. * @param props Configuration parameters. */ public mountAPIKeys = (node: HTMLDivElement, props?: APIKeysProps) => { if (disabledAllAPIKeysFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponent, { code: CANNOT_RENDER_API_KEYS_DISABLED_ERROR_CODE, }); } return; } if (this.organization && disabledOrganizationAPIKeysFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponentForOrgWhenDisabled, { code: CANNOT_RENDER_API_KEYS_ORG_DISABLED_ERROR_CODE, }); } return; } if (!this.organization && disabledUserAPIKeysFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponentForUserWhenDisabled, { code: CANNOT_RENDER_API_KEYS_USER_DISABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'APIKeys'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'apiKeys', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; /** * Unmount an API keys component from the target element. * If there is no component mounted at the target node, results in a noop. * * @param targetNode Target node to unmount the APIKeys component from. */ public unmountAPIKeys = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** * Mount a configure SSO component at the target element. * * @param targetNode Target to mount the ConfigureSSO component. * @param props Configuration parameters. * @hidden */ public __internal_mountConfigureSSO = (node: HTMLDivElement, props?: ConfigureSSOProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'ConfigureSSO', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('ConfigureSSO'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } const userExists = !noUserExists(this); if (noOrganizationExists(this) && userExists) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('ConfigureSSO'), { code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE, }); } return; } if (disabledSelfServeSSOFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderConfigureSSOComponentWhenDisabled, { code: CANNOT_RENDER_SELF_SERVE_SSO_DISABLED_ERROR_CODE, }); } return; } if (disabledEmailAddressAttribute(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderConfigureSSOComponentWhenEmailAddressDisabled, { code: CANNOT_RENDER_CONFIGURE_SSO_EMAIL_ADDRESS_DISABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'ConfigureSSO'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'configureSSO', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; /** * Unmount a configure SSO component from the target element. * If there is no component mounted at the target node, results in a noop. * * @param targetNode Target node to unmount the ConfigureSSO component from. * @hidden */ public __internal_unmountConfigureSSO = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** * Mount the Directory Sync onboarding component at the target element. * Directory Sync provisions members through the organization's SSO connection, * so it requires organizations to be enabled and the self-serve Directory Sync * feature to be turned on for the instance. * * @param targetNode Target to mount the ConfigureDirectorySync component. * @param props Configuration parameters. * @hidden */ public __internal_mountConfigureDirectorySync = (node: HTMLDivElement, props?: ConfigureSSOProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'ConfigureDirectorySync', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('ConfigureDirectorySync'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } const userExists = !noUserExists(this); if (noOrganizationExists(this) && userExists) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('ConfigureDirectorySync'), { code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE, }); } return; } if (disabledSelfServeDirectorySyncFeature(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderConfigureDirectorySyncComponentWhenDisabled, { code: CANNOT_RENDER_SELF_SERVE_SSO_DISABLED_ERROR_CODE, }); } return; } this.assertComponentsReady(this.#clerkUI); const component = 'ConfigureDirectorySync'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'configureDirectorySync', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; /** * Unmount the Directory Sync onboarding component from the target element. * If there is no component mounted at the target node, results in a noop. * * @param targetNode Target node to unmount the ConfigureDirectorySync component from. * @hidden */ public __internal_unmountConfigureDirectorySync = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'TaskChooseOrganization', onClose: () => { throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('TaskChooseOrganization'), { code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE, }); }, }); if (!isOrganizationsEnabled) { return; } this.assertComponentsReady(this.#clerkUI); const component = 'TaskChooseOrganization'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'taskChooseOrganization', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountTaskChooseOrganization = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountTaskResetPassword = (node: HTMLDivElement, props?: TaskResetPasswordProps) => { this.assertComponentsReady(this.#clerkUI); const component = 'TaskResetPassword'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'taskResetPassword', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted('TaskResetPassword', props)); }; public unmountTaskResetPassword = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountTaskSetupMFA = (node: HTMLDivElement, props?: TaskSetupMFAProps) => { this.assertComponentsReady(this.#clerkUI); const component = 'TaskSetupMFA'; void this.#clerkUI .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, appearanceKey: 'taskSetupMfa', node, props, }), ); this.telemetry?.record(eventPrebuiltComponentMounted('TaskSetupMFA', props)); }; public unmountTaskSetupMFA = (node: HTMLDivElement) => { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** * `setActive` can be used to set the active session and/or organization. */ public setActive = async (params: SetActiveParams): Promise => { const { organization, redirectUrl, navigate: setActiveNavigate } = params; let { session } = params; this.__internal_setActiveInProgress = true; debugLogger.debug( 'setActive() start', { hasClient: Boolean(this.client), sessionTarget: typeof session === 'string' ? session : (session?.id ?? session ?? null), organizationTarget: typeof organization === 'string' ? organization : (organization?.id ?? organization ?? null), redirectUrl: redirectUrl ?? null, }, 'clerk', ); try { if (!this.client) { debugLogger.warn('Clerk setActive called before client is loaded', {}, 'clerk'); throw new Error('setActive is being called before the client is loaded. Wait for init.'); } if (session === undefined && !this.session) { debugLogger.warn('Clerk setActive precondition not met: no target session and no active session', {}, 'clerk'); throw new Error( 'setActive should either be called with a session param or there should be already an active session.', ); } if (typeof session === 'string') { const sessionId = session; const findSession = () => (this.client?.sessions.find(x => x.id === sessionId) as SignedInSessionResource | undefined) ?? null; session = findSession(); if (!session) { await this.client.reload(); session = findSession(); } } const onBeforeSetActive: SetActiveHook = typeof window !== 'undefined' && typeof window.__internal_onBeforeSetActive === 'function' ? window.__internal_onBeforeSetActive : noop; const onAfterSetActive: SetActiveHook = typeof window !== 'undefined' && typeof window.__internal_onAfterSetActive === 'function' ? window.__internal_onAfterSetActive : noop; let newSession = session === undefined ? this.session : session; if (newSession?.status === 'pending') { warnMissingPendingTaskHandlers({ ...this.#options, ...params }); } // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. // We now want to set the last active organization id on that session (if it exists). // However, if the `organization` parameter is not given (i.e. `undefined`), we want // to keep the organization id that the session had. const shouldSwitchOrganization = organization !== undefined; if (newSession && shouldSwitchOrganization) { const organizationIdOrSlug = typeof organization === 'string' ? organization : organization?.id; if (isOrganizationId(organizationIdOrSlug)) { newSession.lastActiveOrganizationId = organizationIdOrSlug || null; } else { const matchingOrganization = newSession.user.organizationMemberships.find( mem => mem.organization.slug === organizationIdOrSlug, ); const newLastActiveOrganizationId = matchingOrganization?.organization.id || null; const isPersonalWorkspace = newLastActiveOrganizationId === null; // Do not update in-memory to personal workspace if force organization selection is enabled if (this.environment?.organizationSettings?.forceOrganizationSelection && isPersonalWorkspace) { return; } newSession.lastActiveOrganizationId = newLastActiveOrganizationId; } } // Do not revalidate server cache for pending sessions to avoid unmount of `SignIn/SignUp` AIOs when navigating to task if (newSession?.status !== 'pending') { /** * Hint to each framework, that the user will be signed out when `{session: null}` is provided. */ await onBeforeSetActive(newSession === null ? 'sign-out' : undefined); } const taskUrl = newSession?.status === 'pending' && newSession?.currentTask && this.#options.taskUrls?.[newSession?.currentTask.key]; const shouldNavigate = !!(redirectUrl || taskUrl || setActiveNavigate); const touchIntent: SessionTouchParams['intent'] = shouldSwitchOrganization ? 'select_org' : 'select_session'; //1. setLastActiveSession to passed user session (add a param). // Note that this will also update the session's active organization // id. /* The introduction of useSyncExternalStore introduced a bug here, where the updateClient that happens as a result of this touch emitted and immediately caused components to re-render. Previously, that emit was batched with the setTransitiveState call. This whole block should likely happen later, after the transitive state is set. Because we want to minimize behavioral changes until we can tackle this properly, this was refactored so that updateClient does not emit under these circumstances. */ if (inActiveBrowserTab() || !this.#options.standardBrowser) { let updatedClient: ClientResource | undefined; if (shouldNavigate && newSession) { try { // __internal_touch does not call updateClient automatically updatedClient = await newSession.__internal_touch({ intent: touchIntent }); if (updatedClient) { // We call updateClient manually, but without letting it emit // It's important that the setTransitiveState call happens somewhat // quickly after this, as during this period, the internal Clerk // state does not match the emitted state. this.updateClient(updatedClient, { __internal_dangerouslySkipEmit: true }); } } catch (e) { if (isUnauthenticatedError(e)) { void this.handleUnauthenticated(); } else { throw e; } } } else { await this.#touchCurrentSession(newSession, touchIntent); } // If we do have the updatedClient, read from that, otherwise getSessionFromClient // will fallback to this.client. This makes no difference now, but will if we // decide to move the `updateClient` call out of this block later. newSession = this.#getSessionFromClient(newSession?.id, updatedClient); } // getToken syncs __session and __client_uat to cookies using events.TokenUpdate dispatched event. try { const token = await newSession?.getToken(); if (!token) { eventBus.emit(events.TokenUpdate, { token: null }); } } catch (error) { if (ClerkOfflineError.is(error)) { debugLogger.warn( 'Token fetch failed when setting active session (offline). Preserving existing auth state.', { sessionId: newSession?.id }, 'clerk', ); } else { throw error; } } //2. When navigation is required we emit the session as undefined, // then wait for navigation to finish before emitting the new session. // When undefined, neither SignedIn nor SignedOut renders, which avoids flickers or // automatic reloading when reloading shouldn't be happening. const tracker = createBeforeUnloadTracker(this.#options.standardBrowser); if (shouldNavigate) { await tracker.track(async () => { if (!this.client) { // Typescript is not happy because since thinks this.client might have changed to undefined because the function is asynchronous. return; } if (newSession?.status !== 'pending') { this.#setTransitiveState(); } if (taskUrl) { const taskUrlWithRedirect = redirectUrl ? buildURL({ base: taskUrl, hashSearchParams: { redirectUrl } }, { stringify: true }) : taskUrl; await this.navigate(taskUrlWithRedirect); } else if (setActiveNavigate && newSession) { // Track whether decorateUrl was called for dev-mode warning let decorateUrlCalled = false; const decorateUrl = (url: string): string => { decorateUrlCalled = true; return this.#decorateUrlWithTouch(url); }; await setActiveNavigate({ session: newSession, decorateUrl }); // Warn in development if decorateUrl wasn't called but the client is eligible for touch if (this.#instanceType === 'development' && !decorateUrlCalled && this.client.isEligibleForTouch()) { logger.warnOnce( 'Clerk: The navigate callback in setActive() did not call decorateUrl(). ' + 'In Safari, sessions may be limited to 7 days due to Intelligent Tracking Prevention (ITP). ' + 'Use decorateUrl() to wrap your destination URL to enable the ITP workaround. ' + 'Learn more: https://clerk.com/docs/troubleshooting/safari-itp', ); } } else if (redirectUrl) { await this.navigate(this.#decorateUrlWithTouch(redirectUrl)); } }); } //3. Check if hard reloading (onbeforeunload). If not, set the user/session and emit if (tracker.isUnloading()) { return; } this.#updateAccessors(newSession); // Do not revalidate server cache for pending sessions to avoid unmount of `SignIn/SignUp` AIOs when navigating to task // newSession can be mutated by the time we get here (org change session touch) if (newSession?.status !== 'pending') { await onAfterSetActive(); } } finally { this.__internal_setActiveInProgress = false; } }; public addListener = (listener: ListenerCallback, options?: ListenerOptions): UnsubscribeCallback => { listener = memoizeListenerCallback(listener); this.#listeners.push(listener); // emit right away if (this.client && !options?.skipInitialEmit) { listener({ client: this.client, session: this.session, user: this.user, organization: this.organization, }); } const unsubscribe = () => { this.#listeners = this.#listeners.filter(l => l !== listener); }; return unsubscribe; }; public on: ClerkInterface['on'] = (...args) => { this.#publicEventBus.on(...args); }; public off: ClerkInterface['off'] = (...args) => { this.#publicEventBus.off(...args); }; public __internal_addNavigationListener = (listener: () => void): UnsubscribeCallback => { this.#navigationListeners.push(listener); const unsubscribe = () => { this.#navigationListeners = this.#navigationListeners.filter(l => l !== listener); }; return unsubscribe; }; public navigate = async (to: string | undefined, options?: NavigateOptions): Promise => { if (!to || !inBrowser()) { return; } // In React Native, window exists but window.location does not. // If we have a custom router, use it directly. Otherwise, return early. if (typeof window.location === 'undefined') { const customNavigate = options?.replace && this.#options.routerReplace ? this.#options.routerReplace : this.#options.routerPush; if (customNavigate) { debugLogger.info(`Clerk is navigating to: ${to}`); return await customNavigate(to, { windowNavigate: this.__internal_windowNavigate }); } // No window.location and no custom router - can't navigate if (__DEV__) { console.warn( 'Clerk: Navigation was attempted but window.location is not available and no custom router (routerPush/routerReplace) was provided. ' + 'If you are using React Native, please provide routerPush and routerReplace options to ClerkProvider.', ); } return; } /** * Trigger all navigation listeners. In order for modal UI components to close. */ setTimeout(() => { this.#emitNavigationListeners(); }, 0); let toURL = new URL(to, window.location.href); if (!this.#allowedRedirectProtocols.includes(toURL.protocol)) { console.warn( `Clerk: "${toURL.protocol}" is not a valid protocol. Redirecting to "/" instead. If you think this is a mistake, please open an issue.`, ); toURL = new URL('/', window.location.href); } const customNavigate = options?.replace && this.#options.routerReplace ? this.#options.routerReplace : this.#options.routerPush; debugLogger.info(`Clerk is navigating to: ${toURL}`); if (this.#options.routerDebug) { console.log(`Clerk is navigating to: ${toURL}`); } // Custom protocol URLs have an origin value of 'null'. In many cases, this indicates deep-linking and we want to ensure the customNavigate function is used if available. if ((toURL.origin !== 'null' && toURL.origin !== window.location.origin) || !customNavigate) { this.__internal_windowNavigate(toURL); return; } const metadata = { ...(options?.metadata ? { __internal_metadata: options?.metadata } : {}), windowNavigate: this.__internal_windowNavigate, }; // React router only wants the path, search or hash portion. return await customNavigate(stripOrigin(toURL), metadata); }; public buildUrlWithAuth(to: string): string { if (this.#instanceType === 'production') { return to; } const toURL = new URL(to, window.location.origin); if (toURL.origin === window.location.origin) { return toURL.href; } if (!this.#authService) { return toURL.href; } return this.#authService.decorateUrlWithDevBrowserToken(toURL).href; } public buildSignInUrl(options?: SignInRedirectOptions): string { let redirectUrl = options?.redirectUrl || window.location.href; // For satellites, add sync trigger param to all redirect URLs // This ensures handshake is triggered when returning from primary sign-in if (this.isSatellite) { redirectUrl = this.#addSyncTriggerToUrl(redirectUrl); } const modifiedOptions = this.isSatellite ? this.#addSyncTriggerToRedirectOptions(options) : options; return this.#buildUrl('signInUrl', { ...modifiedOptions, redirectUrl }, options?.initialValues); } public buildSignUpUrl(options?: SignUpRedirectOptions): string { let redirectUrl = options?.redirectUrl || window.location.href; // For satellites, add sync trigger param to all redirect URLs // This ensures handshake is triggered when returning from primary sign-up if (this.isSatellite) { redirectUrl = this.#addSyncTriggerToUrl(redirectUrl); } const modifiedOptions = this.isSatellite ? this.#addSyncTriggerToRedirectOptions(options) : options; return this.#buildUrl('signUpUrl', { ...modifiedOptions, redirectUrl }, options?.initialValues); } /** * Adds the __clerk_synced=false param to a URL to trigger satellite handshake * when the user returns from primary domain after sign-in/sign-up. */ #addSyncTriggerToUrl = (urlString: string): string => { try { const url = new URL(urlString, window.location.origin); url.searchParams.set(CLERK_SYNCED, CLERK_SYNCED_STATUS.NeedsSync); return url.toString(); } catch { // If URL parsing fails, return original return urlString; } }; /** * Adds the __clerk_synced=false param to all redirect URL options for satellites. * This handles forceRedirectUrl and fallbackRedirectUrl variants. */ #addSyncTriggerToRedirectOptions = (options: T): T => { if (!options) { return options; } const result = { ...options } as RedirectOptions; if (result.signInForceRedirectUrl) { result.signInForceRedirectUrl = this.#addSyncTriggerToUrl(result.signInForceRedirectUrl); } if (result.signInFallbackRedirectUrl) { result.signInFallbackRedirectUrl = this.#addSyncTriggerToUrl(result.signInFallbackRedirectUrl); } if (result.signUpForceRedirectUrl) { result.signUpForceRedirectUrl = this.#addSyncTriggerToUrl(result.signUpForceRedirectUrl); } if (result.signUpFallbackRedirectUrl) { result.signUpFallbackRedirectUrl = this.#addSyncTriggerToUrl(result.signUpFallbackRedirectUrl); } return result as T; }; public buildUserProfileUrl(): string { if (!this.environment || !this.environment.displayConfig) { return ''; } return this.buildUrlWithAuth(this.environment.displayConfig.userProfileUrl); } public buildHomeUrl(): string { if (!this.environment || !this.environment.displayConfig) { return ''; } return this.buildUrlWithAuth(this.environment.displayConfig.homeUrl); } public buildAfterSignInUrl({ params }: { params?: URLSearchParams } = {}): string { return this.buildUrlWithAuth(new RedirectUrls(this.#options, {}, params).getAfterSignInUrl()); } public buildAfterSignUpUrl({ params }: { params?: URLSearchParams } = {}): string { return this.buildUrlWithAuth(new RedirectUrls(this.#options, {}, params).getAfterSignUpUrl()); } public buildAfterSignOutUrl(): string { if (!this.#options.afterSignOutUrl) { return '/'; } return this.buildUrlWithAuth(this.#options.afterSignOutUrl); } public buildNewSubscriptionRedirectUrl(): string { if (!this.#options.newSubscriptionRedirectUrl) { return this.buildAfterSignInUrl(); } return this.#options.newSubscriptionRedirectUrl; } public buildWaitlistUrl(options?: { initialValues?: Record }): string { if (!this.environment || !this.environment.displayConfig) { return ''; } const waitlistUrl = this.#options['waitlistUrl'] || this.environment.displayConfig.waitlistUrl; const initValues = new URLSearchParams(options?.initialValues || {}); return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } public buildAfterMultiSessionSingleSignOutUrl(): string { if (!this.environment) { return ''; } if (this.#options.afterMultiSessionSingleSignOutUrl) { return this.buildUrlWithAuth(this.#options.afterMultiSessionSingleSignOutUrl); } if (this.#options.signInUrl) { return this.buildUrlWithAuth( buildURL( { base: this.#options.signInUrl, hashPath: 'choose', }, { stringify: true }, ), ); } return this.buildUrlWithAuth(this.environment.displayConfig.afterSignOutOneUrl); } public buildCreateOrganizationUrl(): string { if (!this.environment || !this.environment.displayConfig) { return ''; } return this.buildUrlWithAuth(this.environment.displayConfig.createOrganizationUrl); } public buildOrganizationProfileUrl(): string { if (!this.environment || !this.environment.displayConfig) { return ''; } return this.buildUrlWithAuth(this.environment.displayConfig.organizationProfileUrl); } public buildTasksUrl(options?: TasksRedirectOptions): string { const currentTask = this.session?.currentTask; if (!currentTask) { return ''; } const customTaskUrl = this.#options.taskUrls?.[currentTask.key]; if (customTaskUrl) { return customTaskUrl; } return buildURL( { base: this.buildSignInUrl(options), hashPath: getTaskEndpoint(currentTask), }, { stringify: true, }, ); } #redirectToSatellite = async (): Promise => { if (!inBrowser()) { return; } // Use __clerk_synced=true to signal sync completion const searchParams = new URLSearchParams({ [CLERK_SYNCED]: CLERK_SYNCED_STATUS.Completed, }); const satelliteUrl = getClerkQueryParam(CLERK_SATELLITE_URL); if (!satelliteUrl || !isHttpOrHttps(satelliteUrl)) { clerkRedirectUrlIsMissingScheme(); } const backToSatelliteUrl = buildURL( { base: getClerkQueryParam(CLERK_SATELLITE_URL) as string, searchParams }, { stringify: true }, ); return this.navigate(this.buildUrlWithAuth(backToSatelliteUrl)); }; public redirectWithAuth = async (to: string): Promise => { if (inBrowser()) { return this.navigate(this.buildUrlWithAuth(to)); } return; }; public redirectToSignIn = async (options?: SignInRedirectOptions): Promise => { if (inBrowser()) { return this.navigate(this.buildSignInUrl(options)); } return; }; public redirectToSignUp = async (options?: SignUpRedirectOptions): Promise => { if (inBrowser()) { return this.navigate(this.buildSignUpUrl(options)); } return; }; public redirectToUserProfile = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildUserProfileUrl()); } return; }; public redirectToCreateOrganization = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildCreateOrganizationUrl()); } return; }; public redirectToOrganizationProfile = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildOrganizationProfileUrl()); } return; }; public redirectToAfterSignIn = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildAfterSignInUrl()); } return; }; public redirectToAfterSignUp = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildAfterSignUpUrl()); } return; }; public redirectToAfterSignOut = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildAfterSignOutUrl()); } return; }; public redirectToWaitlist = async (): Promise => { if (inBrowser()) { return this.navigate(this.buildWaitlistUrl()); } return; }; public redirectToTasks = async (options?: TasksRedirectOptions): Promise => { if (inBrowser()) { return this.navigate(this.buildTasksUrl(options)); } return; }; public handleEmailLinkVerification = async ( params: HandleEmailLinkVerificationParams, customNavigate?: (to: string) => Promise, ): Promise => { if (!this.client) { return; } const verificationStatus = getClerkQueryParam('__clerk_status'); if (verificationStatus === 'expired') { throw new EmailLinkError(EmailLinkErrorCodeStatus.Expired); } else if (verificationStatus === 'client_mismatch') { throw new EmailLinkError(EmailLinkErrorCodeStatus.ClientMismatch); } else if (verificationStatus === 'transferable') { // signUpIfMissing flow: the email was verified but the user doesn't exist, so there is // no session to complete here. The sign-up transfer is banked on the client that owns // the sign-in; consuming it is left to the caller, which knows where to route next. if (typeof params.onVerifiedOnOtherDevice === 'function') { params.onVerifiedOnOtherDevice(); } return; } else if (verificationStatus !== 'verified') { throw new EmailLinkError(EmailLinkErrorCodeStatus.Failed); } const newSessionId = getClerkQueryParam('__clerk_created_session'); const { signIn, signUp, sessions } = this.client; const shouldCompleteOnThisDevice = sessions.some(s => s.id === newSessionId); const shouldContinueOnThisDevice = signIn.status === 'needs_second_factor' || signUp.status === 'missing_requirements'; const navigate = (to: string) => customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); const redirectContinue = params.redirectUrl ? () => navigate(params.redirectUrl as string) : noop; if (shouldCompleteOnThisDevice) { return this.setActive({ session: newSessionId, redirectUrl: params.redirectUrlComplete, }); } else if (shouldContinueOnThisDevice) { return redirectContinue(); } if (typeof params.onVerifiedOnOtherDevice === 'function') { params.onVerifiedOnOtherDevice(); } return null; }; public __internal_handleResourceCallback = async ( signInOrUp: SignInResource | SignUpResource, params: HandleOAuthCallbackParams, customNavigate?: (to: string) => Promise, ): Promise => { if (!this.loaded || !this.environment || !this.client) { return; } const { signIn: _signIn, signUp: _signUp } = this.client; const signIn = 'identifier' in (signInOrUp || {}) ? (signInOrUp as SignInResource) : _signIn; const signUp = 'missingFields' in (signInOrUp || {}) ? (signInOrUp as SignUpResource) : _signUp; // The component router expects raw component-relative paths; buildUrlWithAuth would absolutize // them on development instances and push the navigation back out to the window. const navigate = (to: string) => customNavigate && typeof customNavigate === 'function' ? customNavigate(this.buildUrlWithAuth(to)) : params.__internal_navigate && typeof params.__internal_navigate === 'function' ? params.__internal_navigate(to) : this.navigate(this.buildUrlWithAuth(to)); return this._handleRedirectCallback(params, { signUp, signIn, navigate, }); }; public handleGoogleOneTapCallback = async ( signInOrUp: SignInResource | SignUpResource, params: HandleOAuthCallbackParams, customNavigate?: (to: string) => Promise, ): Promise => { return this.__internal_handleResourceCallback(signInOrUp, params, customNavigate); }; private _handleRedirectCallback = async ( params: ResumeAfterProtectCheckParams, { signIn, signUp, navigate, resuming = false, }: { signIn: SignInResource; signUp: SignUpResource; navigate: (to: string) => Promise; resuming?: boolean; }, ): Promise => { if (!this.loaded || !this.environment || !this.client) { return; } // If `handleRedirectCallback` is called on a window without an opener property (such as when the OAuth flow popup // directs the opening page to navigate to the /sso-callback route), we need to reload the signIn and signUp resources // to ensure that we have the latest state. This operation can fail when we try reloading a resource that doesn't // exist (such as when reloading a signIn resource during a signUp attempt), but this can be safely ignored. if (!window.opener && params.reloadResource) { try { if (params.reloadResource === 'signIn') { await signIn.reload(); } else if (params.reloadResource === 'signUp') { await signUp.reload(); } } catch { // This catch intentionally left blank. } } const { displayConfig } = this.environment; const { firstFactorVerification } = signIn; const { externalAccount } = signUp.verifications; const su = { status: signUp.status, missingFields: signUp.missingFields, externalAccountStatus: externalAccount.status, externalAccountErrorCode: externalAccount.error?.code, externalAccountSessionId: externalAccount.error?.meta?.sessionId, sessionId: signUp.createdSessionId, protectCheck: signUp.protectCheck, }; const si = { status: signIn.status, firstFactorVerificationStatus: firstFactorVerification.status, firstFactorVerificationErrorCode: firstFactorVerification.error?.code, firstFactorVerificationSessionId: firstFactorVerification.error?.meta?.sessionId, sessionId: signIn.createdSessionId, protectCheck: signIn.protectCheck, }; const makeNavigate = (to: string) => () => navigate(to); const navigateToSignIn = makeNavigate(params.signInUrl || displayConfig.signInUrl); const navigateToSignUp = makeNavigate(params.signUpUrl || displayConfig.signUpUrl); const navigateToFactorOne = makeNavigate( params.firstFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-one' }, { stringify: true }), ); const navigateToFactorTwo = makeNavigate( params.secondFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }), ); const navigateToResetPassword = makeNavigate( params.resetPasswordUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/reset-password' }, { stringify: true }), ); const navigateToSignInProtectCheck = makeNavigate( params.signInProtectCheckUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/protect-check' }, { stringify: true }), ); const redirectUrls = new RedirectUrls(this.#options, params); const continueSignUpUrl = params.continueSignUpUrl || buildURL({ base: displayConfig.signUpUrl, hashPath: '/continue' }, { stringify: true }); const verifyEmailAddressUrl = params.verifyEmailAddressUrl || buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-email-address' }, { stringify: true }); const verifyPhoneNumberUrl = params.verifyPhoneNumberUrl || buildURL({ base: displayConfig.signUpUrl, hashPath: '/verify-phone-number' }, { stringify: true }); const signUpProtectCheckUrl = params.signUpProtectCheckUrl || buildURL({ base: displayConfig.signUpUrl, hashPath: '/protect-check' }, { stringify: true }); const signInUrl = params.signInUrl || displayConfig.signInUrl; const signUpUrl = params.signUpUrl || displayConfig.signUpUrl; const enterpriseConnectionsUrl = params.enterpriseConnectionsUrl || buildURL({ base: signUpUrl, hashPath: '/enterprise-connections' }, { stringify: true }); const navigateToSignUpProtectCheck = makeNavigate(signUpProtectCheckUrl); const internalNavigateOnSetActive = params.__internal_navigateOnSetActive; const setActiveNavigate = async ({ session, baseUrl, redirectUrl, }: { session: SessionResource; baseUrl: string; redirectUrl: string; }) => { if (internalNavigateOnSetActive) { await internalNavigateOnSetActive({ session, redirectUrl, decorateUrl: url => this.buildUrlWithAuth(url), }); return; } if (!session.currentTask) { await this.navigate(redirectUrl); return; } await navigateIfTaskExists(session, { baseUrl, navigate: this.navigate, }); }; if (si.status === 'complete') { return this.setActive({ session: si.sessionId, navigate: async ({ session }) => { await setActiveNavigate({ session, baseUrl: signInUrl, redirectUrl: redirectUrls.getAfterSignInUrl(), }); }, }); } // OAuth/SAML callbacks can resolve into a protect_check gate that surfaces on the next // /v1/client read, so check for it here before continuing with the transfer logic below. // Honor either the explicit `protectCheck` field or the `needs_protect_check` status override. // // Scope to the callback's intent: an abandoned sign-in keeps serializing its pending // `protect_check` on the client for up to a day (and a later sign-up doesn't clear it in // multi-session mode), so an unscoped check would route a *sign-up* callback into the stale // sign-in's challenge. We only consult `si` here unless this is explicitly a sign-up callback. // Transfers are unaffected: the `signIn.create({ transfer })` path below checks its own fresh // response for the gate. if (!resuming && params.reloadResource !== 'signUp' && (si.protectCheck || si.status === 'needs_protect_check')) { return navigateToSignInProtectCheck(); } // The sign-up resource can be gated the same way (e.g. a callback that resolves straight into a // gated sign-up). Scope to the sign-up intent for the symmetric reason — a stale sign-up's gate // shouldn't hijack a sign-in callback. if (!resuming && params.reloadResource !== 'signIn' && su.protectCheck) { return navigateToSignUpProtectCheck(); } const userExistsButNeedsToSignIn = su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === ERROR_CODES.EXTERNAL_ACCOUNT_EXISTS; if (userExistsButNeedsToSignIn) { const res = await signIn.create({ transfer: true }); if (res.protectCheck || res.status === 'needs_protect_check') { return navigateToSignInProtectCheck(); } switch (res.status) { case 'complete': return this.setActive({ session: res.createdSessionId, navigate: async ({ session }) => { await setActiveNavigate({ session, baseUrl: signUpUrl, redirectUrl: redirectUrls.getAfterSignInUrl(), }); }, }); case 'needs_first_factor': return navigateToFactorOne(); case 'needs_second_factor': return navigateToFactorTwo(); case 'needs_new_password': return navigateToResetPassword(); default: clerkOAuthCallbackDidNotCompleteSignInSignUp('sign in'); } } const userLockedFromSignUp = su.externalAccountErrorCode === 'user_locked'; const userLockedFromSignIn = si.firstFactorVerificationErrorCode === 'user_locked'; if (userLockedFromSignUp) { return navigateToSignUp(); } if (userLockedFromSignIn) { return navigateToSignIn(); } const userHasUnverifiedEmail = si.status === 'needs_first_factor' && !signIn.supportedFirstFactors?.every(f => f.strategy === 'enterprise_sso'); if (userHasUnverifiedEmail) { return navigateToFactorOne(); } const userNeedsNewPassword = si.status === 'needs_new_password'; if (userNeedsNewPassword) { return navigateToResetPassword(); } const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable' || params.continuation === 'transfer_to_sign_up'; if (userNeedsToBeCreated) { if (params.transferable === false) { return navigateToSignIn(); } const res = await signUp.create({ transfer: true, unsafeMetadata: params.unsafeMetadata }); switch (res.status) { case 'complete': return this.setActive({ session: res.createdSessionId, navigate: async ({ session }) => { await setActiveNavigate({ session, baseUrl: signUpUrl, redirectUrl: redirectUrls.getAfterSignUpUrl(), }); }, }); case 'missing_requirements': return navigateToNextStepSignUp({ signUp: res, continueSignUpUrl, verifyEmailAddressUrl, verifyPhoneNumberUrl, signUpProtectCheckUrl, enterpriseConnectionsUrl, navigate, }); default: clerkOAuthCallbackDidNotCompleteSignInSignUp('sign in'); } } if (su.status === 'complete') { return this.setActive({ session: su.sessionId, navigate: async ({ session }) => { await setActiveNavigate({ session, baseUrl: signUpUrl, redirectUrl: redirectUrls.getAfterSignUpUrl(), }); }, }); } if (si.status === 'needs_second_factor') { return navigateToFactorTwo(); } const suUserAlreadySignedIn = (su.externalAccountStatus === 'failed' || su.externalAccountStatus === 'unverified') && su.externalAccountErrorCode === 'identifier_already_signed_in' && su.externalAccountSessionId; const siUserAlreadySignedIn = si.firstFactorVerificationStatus === 'failed' && si.firstFactorVerificationErrorCode === 'identifier_already_signed_in' && si.firstFactorVerificationSessionId; const userAlreadySignedIn = suUserAlreadySignedIn || siUserAlreadySignedIn; if (userAlreadySignedIn) { const sessionId = si.firstFactorVerificationSessionId || su.externalAccountSessionId; if (sessionId) { return this.setActive({ session: sessionId, navigate: async ({ session }) => { await setActiveNavigate({ session, baseUrl: suUserAlreadySignedIn ? signUpUrl : signInUrl, redirectUrl: redirectUrls.getAfterSignInUrl(), }); }, }); } } if (hasExternalAccountSignUpError(signUp)) { return navigateToSignUp(); } if (su.externalAccountStatus === 'verified' && su.status === 'missing_requirements') { return navigateToNextStepSignUp({ signUp, continueSignUpUrl, verifyEmailAddressUrl, verifyPhoneNumberUrl, signUpProtectCheckUrl, enterpriseConnectionsUrl, navigate, }); } if (this.session?.currentTask) { await this.redirectToTasks({ redirectUrl: this.buildAfterSignInUrl(), }); return; } const userMustChooseEnterpriseConnection = params.reloadResource !== 'signUp' && si.status === 'needs_first_factor' && hasMultipleEnterpriseConnections(signIn.supportedFirstFactors); if (userMustChooseEnterpriseConnection) { return navigateToFactorOne(); } return navigateToSignIn(); }; public __internal_resumeAfterProtectCheck = async ( params: ResumeAfterProtectCheckParams = {}, customNavigate?: (to: string) => Promise, ): Promise => { if (!this.loaded || !this.environment || !this.client) { return; } const { signIn, signUp } = this.client; const resolvedNavigate = customNavigate ?? params.__internal_navigate; const navigate = (to: string) => resolvedNavigate && typeof resolvedNavigate === 'function' ? resolvedNavigate(to) : this.navigate(to); return this._handleRedirectCallback(params, { signUp, signIn, navigate, resuming: true, }); }; public handleRedirectCallback = async ( params: HandleOAuthCallbackParams = {}, customNavigate?: (to: string) => Promise, ): Promise => { if (!this.loaded || !this.environment || !this.client) { return; } // A callback's transfer can only succeed once, so if we get called again, for example because a // component remounted, we return the running promise. // We intentionally ignore params/customNavigate here, there is no way to cancel the first call // or queue up a second one safely, so the only other options would be to fail the second call // or have it race the first one with unpredictable effects. // Reusing the first call's params is always preferable to that. if (this.#redirectCallbackInFlight) { return this.#redirectCallbackInFlight; } const { signIn, signUp } = this.client; const resolvedNavigate = customNavigate ?? params.__internal_navigate; const navigate = (to: string) => resolvedNavigate && typeof resolvedNavigate === 'function' ? resolvedNavigate(to) : this.navigate(to); this.#redirectCallbackInFlight = this._handleRedirectCallback(params, { signUp, signIn, navigate, }).finally(() => { this.#redirectCallbackInFlight = undefined; }); return this.#redirectCallbackInFlight; }; // TODO: Deprecate this one, and mark it as internal. Is there actual benefit for external developers to use this ? Should they ever reach for it ? public handleUnauthenticated = async (opts = { broadcast: true }): Promise => { if (!this.client || !this.session) { return; } try { const newClient = await Client.getOrCreateInstance().fetch(); this.updateClient(newClient); if (this.session) { return; } if (opts.broadcast) { eventBus.emit(events.UserSignOut, null); } return this.setActive({ session: null }); } catch (err) { // `/client` can fail with either a 401, a 403, 500 or network errors. // 401 is already handled internally in our fetcher. // 403 means that the client is blocked, signing out the user is the only option. // 500 means that the client is not working, signing out the user is the only option, since the intention was to sign out the user. if (isClerkAPIResponseError(err) && [403, 500].includes(err.status)) { return this.setActive({ session: null }); } else { throw err; } } }; public __internal_handleUnauthenticatedDevBrowser = async (): Promise => { if (this.#authService) { await this.#authService.handleUnauthenticatedDevBrowser(); } }; public authenticateWithGoogleOneTap = async ( params: AuthenticateWithGoogleOneTapParams, ): Promise => { if (__BUILD_DISABLE_RHC__) { clerkUnsupportedEnvironmentWarning('Google One Tap'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.client!.signIn; // TODO: Remove not null assertion } return this.client?.signIn .create({ strategy: 'google_one_tap', token: params.token, }) .catch(err => { if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') { return this.client?.signUp.create({ strategy: 'google_one_tap', token: params.token, legalAccepted: params.legalAccepted, }); } throw err; }) as Promise; }; public authenticateWithMetamask = async (props: AuthenticateWithMetamaskParams = {}): Promise => { await this.authenticateWithWeb3({ ...props, strategy: 'web3_metamask_signature', }); }; public authenticateWithCoinbaseWallet = async (props: AuthenticateWithCoinbaseWalletParams = {}): Promise => { await this.authenticateWithWeb3({ ...props, strategy: 'web3_coinbase_wallet_signature', }); }; public authenticateWithBase = async (props: AuthenticateWithBaseParams = {}): Promise => { await this.authenticateWithWeb3({ ...props, strategy: 'web3_base_signature', }); }; public authenticateWithOKXWallet = async (props: AuthenticateWithOKXWalletParams = {}): Promise => { await this.authenticateWithWeb3({ ...props, strategy: 'web3_okx_wallet_signature', }); }; public authenticateWithSolana = async (props: AuthenticateWithSolanaParams): Promise => { await this.authenticateWithWeb3({ ...props, strategy: 'web3_solana_signature', }); }; public authenticateWithWeb3 = async ({ redirectUrl, signUpContinueUrl, customNavigate, unsafeMetadata, strategy, legalAccepted, secondFactorUrl, protectCheckUrl, signUpProtectCheckUrl, walletName, }: ClerkAuthenticateWithWeb3Params): Promise => { if (!this.client || !this.environment) { return; } const { displayConfig } = this.environment; const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; const identifier = await web3().getWeb3Identifier({ provider, walletName }); let generateSignature: GenerateSignature; switch (provider) { case 'metamask': generateSignature = web3().generateSignatureWithMetamask; break; case 'base': generateSignature = web3().generateSignatureWithBase; break; case 'coinbase_wallet': generateSignature = web3().generateSignatureWithCoinbaseWallet; break; case 'solana': if (!walletName) { throw new ClerkRuntimeError('Wallet name is required for Solana authentication.', { code: 'web3_solana_wallet_name_required', }); } // Solana requires walletName; bind it into the helper generateSignature = params => web3().generateSignatureWithSolana({ ...params, walletName }); break; default: generateSignature = web3().generateSignatureWithOKXWallet; break; } const makeNavigate = (to: string) => () => customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); const navigateToFactorTwo = makeNavigate( secondFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }), ); const navigateToSignInProtectCheck = makeNavigate( protectCheckUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/protect-check' }, { stringify: true }), ); const navigateToSignUpProtectCheck = makeNavigate( signUpProtectCheckUrl || buildURL({ base: displayConfig.signUpUrl, hashPath: '/protect-check' }, { stringify: true }), ); const navigateToContinueSignUp = makeNavigate( signUpContinueUrl || buildURL( { base: displayConfig.signUpUrl, hashPath: '/continue', }, { stringify: true }, ), ); let signInOrSignUp: SignInResource | SignUpResource; let viaSignUp = false; try { signInOrSignUp = await this.client.signIn.authenticateWithWeb3({ identifier, generateSignature, strategy, walletName, }); } catch (err) { if (isError(err, ERROR_CODES.FORM_IDENTIFIER_NOT_FOUND)) { viaSignUp = true; signInOrSignUp = await this.client.signUp.authenticateWithWeb3({ identifier, generateSignature, unsafeMetadata, strategy, legalAccepted, walletName, }); if ( signUpContinueUrl && signInOrSignUp.status === 'missing_requirements' && signInOrSignUp.verifications.web3Wallet.status === 'verified' && // A protect_check gate also surfaces as missing_requirements; don't skip past it into // the continue step. The gate is handled by the sign-up protect-check route instead. !signInOrSignUp.protectCheck ) { await navigateToContinueSignUp(); } } else { throw err; } } const setActiveNavigate = async ({ session, redirectUrl }: { session: SessionResource; redirectUrl: string }) => { if (!session.currentTask) { await this.navigate(redirectUrl); return; } await navigateIfTaskExists(session, { baseUrl: displayConfig.signInUrl, navigate: this.navigate, }); }; // A Clerk Protect challenge can gate the inline web3 attempt (no redirect happens, so the // centralized _handleRedirectCallback check never runs). Route to the challenge before the // status switch below, otherwise the user is stranded on the wallet step. The sign-up fallback // gates as `missing_requirements` + `protectCheck`, so it has no status branch below either. if (signInOrSignUp.protectCheck || signInOrSignUp.status === 'needs_protect_check') { await (viaSignUp ? navigateToSignUpProtectCheck : navigateToSignInProtectCheck)(); return; } switch (signInOrSignUp.status) { case 'needs_second_factor': await navigateToFactorTwo(); break; case 'complete': if (signInOrSignUp.createdSessionId) { await this.setActive({ session: signInOrSignUp.createdSessionId, navigate: async ({ session }) => { await setActiveNavigate({ session, redirectUrl: redirectUrl ?? this.buildAfterSignInUrl() }); }, }); } break; default: return; } }; public createOrganization = async ({ name, slug }: CreateOrganizationParams): Promise => { return Organization.create({ name, slug }); }; public getOrganization = async (organizationId: string): Promise => Organization.get(organizationId); public joinWaitlist = async ({ emailAddress }: JoinWaitlistParams): Promise => Waitlist.join({ emailAddress }); public updateEnvironment(environment: EnvironmentResource): asserts this is { environment: EnvironmentResource } { this.environment = environment; } __internal_setCountry = (country: string | null) => { if (!this.__internal_country) { this.__internal_country = country; } }; get __internal_last_error(): ClerkAPIError | null { const value = this.internal_last_error; this.internal_last_error = null; return value; } set __internal_last_error(value: ClerkAPIError | null) { this.internal_last_error = value; } // Skipping emit is dangerous because if there is a gap between setting these // and emitting, library consumers that both read state directly and set up listeners // could end up in a inconsistent state. updateClient = (newClient: ClientResource, options?: { __internal_dangerouslySkipEmit?: boolean }): void => { if (!this.client) { // This is the first time client is being // set, so we also need to set session const session = this.#options.selectInitialSession ? this.#options.selectInitialSession(newClient) : this.#defaultSession(newClient); this.#updateAccessors(session, { dangerouslySkipEmit: true }); } this.client = newClient; if (this.session) { const newSession = this.#getSessionFromClient(this.session.id, newClient); const hasTransitionedToPendingStatus = this.session.status === 'active' && newSession?.status === 'pending'; if (hasTransitionedToPendingStatus) { const onAfterSetActive: SetActiveHook = typeof window !== 'undefined' && typeof window.__internal_onAfterSetActive === 'function' ? window.__internal_onAfterSetActive : noop; // Execute hooks to update server authentication context and trigger // page protections in clerkMiddleware or server components void onAfterSetActive(); } // Note: this might set this.session to null // We need to set these values before emitting the token update, as handling that relies on these values being set. // We don't want to emit here though, as we want to emit the token update first. That happens synchronously, so it // should be safe as long as we call #emit() right after. this.#updateAccessors(newSession, { dangerouslySkipEmit: true }); // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. if (!this.session?.lastActiveToken && !isValidBrowserOnline()) { debugLogger.warn( 'No last active token when updating client (offline)', { sessionId: this.session?.id }, 'clerk', ); } eventBus.emit(events.TokenUpdate, { token: this.session?.lastActiveToken }); } if (!options?.__internal_dangerouslySkipEmit) { this.#emit(); } }; get __internal_environment(): EnvironmentResource | null | undefined { return this.environment; } // TODO: Fix this properly // eslint-disable-next-line @typescript-eslint/require-await __internal_setEnvironment = async (env: EnvironmentJSON) => { this.environment = new Environment(env); // TODO @nikos update // if (Clerk.mountComponentRenderer) { // this.#componentRenderer = Clerk.mountComponentRenderer(this, this.environment, this.#options); // } }; __internal_onBeforeRequest = (callback: FapiRequestCallback): void => { this.#fapiClient.onBeforeRequest(callback); }; __internal_onAfterResponse = (callback: FapiRequestCallback): void => { this.#fapiClient.onAfterResponse(callback); }; // TODO @userland-errors: __internal_updateProps = (_props: any) => { // We need to re-init the options here in order to keep the options passed to ClerkProvider // in sync with the state of clerk-js. If we don't init the options here again, the following scenario is possible: // 1. User renders // 2. clerk-js initializes propA with a default value // 3. The customer update propB independently of propA and window.Clerk.updateProps is called // 4. If we don't merge the new props with the current options, propA will be reset to undefined const props = { ..._props, options: this.#initOptions({ ...this.#options, ..._props.options }), }; return this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.updateProps(props)); }; __internal_navigateWithError(to: string, err: ClerkAPIError) { this.__internal_last_error = err; return this.navigate(to); } #buildSyncUrlForDevelopmentInstances = (): string => { const searchParams = new URLSearchParams({ [CLERK_SATELLITE_URL]: window.location.href, }); return buildURL({ base: this.#options.signInUrl, searchParams }, { stringify: true }); }; #buildSyncUrlForProductionInstances = (): string => { let primarySyncUrl; if (this.proxyUrl) { const proxy = new URL(this.proxyUrl); primarySyncUrl = new URL(`${proxy.pathname}/v1/client/sync`, proxy.origin); } else if (this.domain) { primarySyncUrl = new URL(`/v1/client/sync`, `https://${this.domain}`); } primarySyncUrl?.searchParams.append('redirect_url', window.location.href); return primarySyncUrl?.toString() || ''; }; #shouldSyncWithPrimary = (): boolean => { // Check sync status: __clerk_synced=true means completed, __clerk_synced=false means needs sync const syncStatus = getClerkQueryParam(CLERK_SYNCED); if (syncStatus === CLERK_SYNCED_STATUS.Completed) { return false; } if (!this.isSatellite) { return false; } // Check if sync was triggered (e.g., after returning from primary sign-in) const needsSync = syncStatus === CLERK_SYNCED_STATUS.NeedsSync; if (needsSync) { return true; } // Check if satelliteAutoSync is enabled - only auto-sync when explicitly opted in // In Core 3, satelliteAutoSync defaults to false (undefined is treated as false) if (this.#options.satelliteAutoSync !== true) { return false; } return !!this.#authService?.isSignedOut(); }; #shouldRedirectToSatellite = (): boolean => { if (this.#instanceType === 'production') { return false; } if (this.isSatellite) { return false; } const satelliteUrl = getClerkQueryParam(CLERK_SATELLITE_URL); return !!satelliteUrl; }; #syncWithPrimary = async () => { if (this.instanceType === 'development') { await this.navigate(this.#buildSyncUrlForDevelopmentInstances()); } else if (this.instanceType === 'production') { await this.navigate(this.#buildSyncUrlForProductionInstances()); } }; #assertSignInFormatAndOrigin = (_signInUrl: string, origin: string) => { let signInUrl: URL; try { signInUrl = new URL(_signInUrl); } catch { clerkInvalidSignInUrlFormat(); } if (signInUrl.origin === origin) { clerkInvalidSignInUrlOrigin(); } }; #validateMultiDomainOptions = () => { if (!this.isSatellite) { return; } if (this.#instanceType === 'development' && !this.#options.signInUrl) { clerkMissingSignInUrlAsSatellite(); } if (!this.proxyUrl && !this.domain) { clerkMissingProxyUrlAndDomain(); } if (this.#options.signInUrl) { this.#assertSignInFormatAndOrigin(this.#options.signInUrl, window.location.origin); } }; #loadInStandardBrowser = async (): Promise => { /** * 0. Init auth service and setup dev browser * This is not needed for production instances hence the .clear() * At this point we have already attempted to pre-populate devBrowser, if Step 2 was successful this will not be overwritten. * For multi-domain we want to avoid retrieving a fresh dev browser from FAPI, and we need to get the token as a result of multi-domain session syncing. */ this.#authService = await AuthCookieService.create( this, this.#fapiClient, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.#instanceType!, this.#publicEventBus, ); /** * 1. Multi-domain SSO handling * If needed the app will attempt to sync with another app hosted in a different domain in order to acquire a session * - for development instances it populates the dev browser and `devBrowserHandler.setup()` should not have run. */ this.#validateMultiDomainOptions(); if (this.#shouldSyncWithPrimary()) { await this.#syncWithPrimary(); // ClerkJS is not considered loaded during the sync/link process with the primary domain, return early return; } /** * 3. If the app is considered a primary domain and is in the middle of the sync/link flow, interact the loading of Clerk and redirect back to the satellite app * Initially step 2 and 4 were considered one but for step 2 we need devBrowserHandler.setup() to not have run and step 4 requires a valid dev browser */ if (this.#shouldRedirectToSatellite()) { await this.#redirectToSatellite(); return; } /** * 4. Continue with clerk-js setup. * - Fetch & update environment * - Fetch & update client * - Mount components */ this.#pageLifecycle = createPageLifecycle(); if (typeof BroadcastChannel !== 'undefined') { this.#broadcastChannel = new BroadcastChannel('clerk'); } this.#setupBrowserListeners(); const isInAccountsHostedPages = isDevAccountPortalOrigin(window?.location.hostname); const shouldTouchEnv = this.#instanceType === 'development' && !isInAccountsHostedPages; let initializationDegradedCounter = 0; let retries = 0; while (retries < 2) { retries++; try { const initEnvironmentPromise = Environment.getInstance() .fetch({ touch: shouldTouchEnv }) .then(res => this.updateEnvironment(res)) .catch(err => { if (isError(err, 'dev_browser_unauthenticated')) { throw err; } ++initializationDegradedCounter; const environmentSnapshot = SafeLocalStorage.getItem( CLERK_ENVIRONMENT_STORAGE_ENTRY, null, ); if (environmentSnapshot) { this.updateEnvironment(new Environment(environmentSnapshot)); } }); const initClient = async () => { // Abort the /client request on timeout so clerkjs loading does not hang const clientFetchController = new AbortController(); return timeLimit( Client.getOrCreateInstance().fetch({ abortSignal: clientFetchController.signal }), INITIALIZATION_TIMEOUT_MS, clientFetchController, ) .then(res => this.updateClient(res)) .catch(async e => { /** * Only handle non 4xx errors, like 5xx errors and network errors. */ if (is4xxError(e)) { // bubble it up throw e; } ++initializationDegradedCounter; /** * In most scenarios we want the poller to stop while we are fetching a fresh token during an outage. * We want to avoid having the below `getToken()` retrying at the same time as the poller. */ this.#authService?.stopPollingForToken(); try { const session = this.#defaultSession(createClientFromJwt(this.#authService?.getSessionCookie())); // Prefer minting a fresh token as its claims are fresher than the cookie's session?.clearCache(); const jwt = session ? await timeLimit(session.getToken(), INITIALIZATION_TIMEOUT_MS).catch(() => { session.clearCache(); return this.#authService?.getSessionCookie(); }) : this.#authService?.getSessionCookie(); this.updateClient(createClientFromJwt(jwt)); } finally { this.#authService?.startPollingForToken(); } void Client.getOrCreateInstance() .fetch() .then(res => this.updateClient(res)) .catch(noop); return null; }); }; const [environmentResult, clientResult] = await allSettled([initEnvironmentPromise, initClient()]); if (environmentResult.status === 'rejected') { throw environmentResult.reason; } if (clientResult.status === 'rejected') { const e = clientResult.reason; if (isError(e, 'requires_captcha')) { await initClient(); } else { throw e; } } this.#authService?.setClientUatCookieForDevelopmentInstances(); if (await this.#redirectFAPIInitiatedFlow()) { return; } break; } catch (err) { if (isError(err, 'dev_browser_unauthenticated')) { await this.#authService.handleUnauthenticatedDevBrowser(); } else if (!isValidBrowserOnline()) { console.warn(err); return; } else { throw err; } } if (retries >= 2) { clerkErrorInitFailed(); } } this.#captchaHeartbeat = new CaptchaHeartbeat(this); void this.#captchaHeartbeat.start(); this.#clearClerkQueryParams(); this.#handleImpersonationFab(); this.#handleKeylessPrompt(); this.#publicEventBus.emit(clerkEvents.Status, initializationDegradedCounter > 0 ? 'degraded' : 'ready'); }; private shouldFallbackToCachedResources = (): boolean => { return !!this.__internal_getCachedResources; }; #loadInNonStandardBrowser = async (): Promise => { let environment: Environment, client: Client; const fallsBackToCachedResources = this.shouldFallbackToCachedResources(); let initializationDegradedCounter = 0; try { [environment, client] = await this.#fetchInitialResources( fallsBackToCachedResources ? { fetchMaxTries: 1, timeLimited: true } : { timeLimited: false }, ); } catch (err) { const isUnavailable = (isClerkRuntimeError(err) && err.code === 'network_error') || (isClerkAPIResponseError(err) && err.status >= 500); if (isUnavailable && fallsBackToCachedResources) { const cachedResources = await this.__internal_getCachedResources?.(); environment = new Environment(cachedResources?.environment); Client.clearInstance(); client = Client.getOrCreateInstance(cachedResources?.client); ++initializationDegradedCounter; } else { throw err; } } this.updateClient(client); this.updateEnvironment(environment); // TODO: Add an auth service also for non standard browsers that will poll for the __session JWT but won't use cookies this.#publicEventBus.emit(clerkEvents.Status, initializationDegradedCounter > 0 ? 'degraded' : 'ready'); }; #fetchInitialResources = ({ fetchMaxTries, timeLimited, }: { fetchMaxTries?: number; timeLimited: boolean; }): Promise<[Environment, Client]> => { const controller = new AbortController(); const resources = Promise.all([ Environment.getInstance().fetch({ touch: false, fetchMaxTries, abortSignal: controller.signal }), Client.getOrCreateInstance().fetch({ fetchMaxTries, abortSignal: controller.signal }), ]); if (!timeLimited) { return resources; } return timeLimit(resources, INITIALIZATION_TIMEOUT_MS, controller).catch(err => { if (controller.signal.aborted) { throw new ClerkRuntimeError('Loading the initial resources timed out.', { code: 'network_error' }); } throw err; }); }; // This is used by @clerk/expo. When `load()` failed, such as while the app version was unsupported, a successful // reload finishes loading this instance instead of the app having to create a new one. __internal_reloadInitialResources = async (): Promise => { const [environment, client] = await this.#fetchInitialResources({ fetchMaxTries: 1, timeLimited: true }); this.updateClient(client); this.updateEnvironment(environment); // A load that is still running finishes with these resources if it fails. An overlapping reload may have finished // loading first. if (!this.#options.standardBrowser && this.#status === 'error') { this.#finishLoad(); this.#publicEventBus.emit(clerkEvents.Status, 'ready'); } else if (this.#status === 'loading') { this.#initialResourcesReloadedDuringLoad = true; } this.#emit(); }; #defaultSession = (client: ClientResource): SignedInSessionResource | null => { if (client.lastActiveSessionId) { const currentSession = client.signedInSessions.find(s => s.id === client.lastActiveSessionId); if (currentSession) { return currentSession; } } const session = client.signedInSessions[0]; return session || null; }; #setupBrowserListeners = (): void => { if (!inClientSide()) { return; } this.#pageLifecycle?.onPageFocus(() => { if (!this.session) { return; } // In multi-session apps, it's possible that different tabs will have different active sessions. It's critical that the tab's active session is touched in this case so the session is properly updated on the backend, and so we avoid any throttling when multi-session mode is enabled. const multisessionMode = this.environment && !this.environment.authConfig.singleSessionMode; if (!multisessionMode && this.#touchThrottledUntil > Date.now()) { return; } this.#touchThrottledUntil = Date.now() + 5_000; if (this.#options.touchSession) { // Even if touch fails, we're still in a generally good state that can recover. // There are some caveats and edge cases, like if you reload the tab after a failed // touch in a multi-tab scenario, you might get the last active user/org that was // recorded by the other tab, but that's not catastrophic. // We were previously not swallowing errors here, which led to unnecessary uncaught // error logs in the browser console and noise in error tracking tools. // This is a POST and does not currently retry, we could reconsider that if we wanted // to, but probably only leads to unnecessary complexity for little gain. this.#touchCurrentSession(this.session, 'focus').catch(error => { debugLogger.warn('Session touch on page focus failed', { error }, 'clerk'); }); } }); /** * Background tabs get notified of cross-tab signout events. */ this.#broadcastChannel?.addEventListener('message', (event: MessageEvent) => { if (event.data?.type === 'signout') { void this.handleUnauthenticated({ broadcast: false }); } }); /** * Allow resources within the singleton to notify other tabs about a signout event (scoped to a single tab) */ eventBus.on(events.UserSignOut, () => { this.#broadcastChannel?.postMessage({ type: 'signout' }); }); eventBus.on(events.EnvironmentUpdate, () => { // Cache the environment snapshot for 24 hours SafeLocalStorage.setItem( CLERK_ENVIRONMENT_STORAGE_ENTRY, this.environment?.__internal_toSnapshot(), 24 * 60 * 60 * 1_000, ); }); }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc #touchCurrentSession = async ( session?: SignedInSessionResource | null, intent: SessionTouchParams['intent'] = 'focus', ): Promise => { if (!session) { return Promise.resolve(); } await session.touch({ intent }).catch(e => { if (isUnauthenticatedError(e)) { void this.handleUnauthenticated(); } else { throw e; } }); }; public __internal_lastEmittedResources: Resources | undefined; #emit = (): void => { if (this.client) { const resources = { client: this.client, session: this.session, user: this.user, organization: this.organization, }; this.__internal_lastEmittedResources = resources; for (const listener of this.#listeners) { listener(resources); } } }; #emitNavigationListeners = (): void => { for (const listener of this.#navigationListeners) { listener(); } }; /** * Temporarily clears the accessors before emitting changes to React context state. * This is used during transitions like sign-out or session changes to prevent UI flickers * such as unexpected unmount of control components */ #setTransitiveState = () => { this.session = undefined; this.organization = undefined; this.user = undefined; this.#emit(); }; #getLastActiveOrganizationFromSession = (session = this.session) => { const orgMemberships = session?.user.organizationMemberships || []; return orgMemberships.map(om => om.organization).find(org => org.id === session?.lastActiveOrganizationId) || null; }; #getAccessorsFromSession = (session = this.session) => { return { session: session || null, organization: this.#getLastActiveOrganizationFromSession(session), user: session ? session.user : null, }; }; /** * Updates the accessors for the Clerk singleton and emits. * If dangerouslySkipEmit is true, the emit will be skipped and you are responsible for calling #emit() yourself. This is dangerous because if there is a gap between setting these and emitting, library consumers that both read state directly and set up listeners could end up in a inconsistent state. * * New code should avoid using dangerouslySkipEmit. If you need to use the new values for something before emitting, instead use a "work in progress" state with the new values that you pass around and act on until it's time to "commit" it by calling #updateAccessors with the new values. */ #updateAccessors = (session?: SignedInSessionResource | null, options?: { dangerouslySkipEmit?: boolean }) => { const { session: newSession, organization, user } = this.#getAccessorsFromSession(session); this.session = newSession; this.organization = organization; this.user = user; if (!options?.dangerouslySkipEmit) { this.#emit(); } }; #getSessionFromClient = (sessionId: string | undefined, client = this.client): SignedInSessionResource | null => { return client?.signedInSessions.find(x => x.id === sessionId) || null; }; #handleImpersonationFab = () => { this.addListener(({ session }) => { const isImpersonating = !!session?.actor; if (isImpersonating) { void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.mountImpersonationFab()); } }); }; #handleKeylessPrompt = () => { if (this.#options.__internal_keyless_claimKeylessApplicationUrl) { void this.#clerkUI ?.then(ui => ui.ensureMounted()) .then(controls => { // TODO(@pantelis): Investigate if this resets existing props controls.updateProps({ options: { __internal_keyless_claimKeylessApplicationUrl: this.#options.__internal_keyless_claimKeylessApplicationUrl, __internal_keyless_copyInstanceKeysUrl: this.#options.__internal_keyless_copyInstanceKeysUrl, __internal_keyless_dismissPrompt: this.#options.__internal_keyless_dismissPrompt, }, }); }); } }; #buildUrl = ( key: 'signInUrl' | 'signUpUrl', options: RedirectOptions, _initValues?: Record, ): string => { if (!key || !this.loaded || !this.environment || !this.environment.displayConfig) { return ''; } let signInOrUpUrl = this.#options[key] || this.environment.displayConfig[key]; if (this.#isCombinedSignInOrUpFlow()) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion signInOrUpUrl = this.#options.signInUrl!; } const redirectUrls = new RedirectUrls(this.#options, options).toSearchParams(); const initValues = new URLSearchParams(_initValues || {}); const url = buildURL( { base: signInOrUpUrl, hashPath: this.#isCombinedSignInOrUpFlow() && key === 'signUpUrl' ? '/create' : '', hashSearchParams: [initValues, redirectUrls], }, { stringify: true }, ); return this.buildUrlWithAuth(url); }; assertComponentsReady(val: unknown): asserts val is ClerkUI { if (!val) { throw new Error('Clerk was not loaded with Ui components'); } } #redirectFAPIInitiatedFlow = async (): Promise => { const redirectUrl = new URLSearchParams(window.location.search).get('redirect_url'); const isProdInstance = this.instanceType === 'production'; const shouldRedirect = redirectUrl !== null && isRedirectForFAPIInitiatedFlow(this.frontendApi, redirectUrl); if (isProdInstance || !shouldRedirect) { return false; } const userSignedIn = this.session; const signInUrl = this.#options.signInUrl || this.environment?.displayConfig.signInUrl; const referrerIsSignInUrl = signInUrl && window.location.href.startsWith(signInUrl); const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; const referrerIsSignUpUrl = signUpUrl && window.location.href.startsWith(signUpUrl); // don't redirect if user is not signed in and referrer is sign in/up url if (requiresUserInput(redirectUrl) && !userSignedIn && (referrerIsSignInUrl || referrerIsSignUpUrl)) { return false; } await this.navigate(this.buildUrlWithAuth(redirectUrl)); return true; }; #initOptions = (options?: ClerkOptions): ClerkOptions => { // Support legacy clerkUICtor / clerkUiCtor options from older SDK versions. // Convert to the new ui.ClerkUI format so the rest of the codebase only checks one path. const legacy = options as Record | undefined; const legacyCtor = legacy?.clerkUICtor ?? legacy?.clerkUiCtor; const ui = legacyCtor ? { ...options?.ui, ClerkUI: legacyCtor as NonNullable['ClerkUI'] } : options?.ui; return { ...defaultOptions, ...options, ui, allowedRedirectOrigins: createAllowedRedirectOrigins( options?.allowedRedirectOrigins, this.frontendApi, this.instanceType, ), }; }; /** * The handshake payload is transported in the URL in development. In cases where FAPI is returning the handshake payload, but Clerk is being used in a client-only application, * we remove the handshake associated parameters as they are not necessary. */ #clearClerkQueryParams = () => { try { removeClerkQueryParam(CLERK_SYNCED); removeClerkQueryParam(CLERK_NETLIFY_CACHE_BUST_PARAM); // @nikos: we're looking into dropping this param completely // in the meantime, we're removing it here to keep the URL clean removeClerkQueryParam(CLERK_SUFFIXED_COOKIES); removeClerkQueryParam('__clerk_handshake'); removeClerkQueryParam('__clerk_handshake_nonce'); removeClerkQueryParam('__clerk_help'); } catch { // ignore } }; get #allowedRedirectProtocols() { let allowedProtocols = ALLOWED_PROTOCOLS; if (this.#options.allowedRedirectProtocols) { allowedProtocols = allowedProtocols.concat(this.#options.allowedRedirectProtocols); } return allowedProtocols; } /** * Primary `window.location.href` navigation chokepoint for `@clerk/clerk-js` and `@clerk/ui`. * By default the resolved URL is validated against the customer-supplied * `allowedRedirectProtocols` option (the static `ALLOWED_PROTOCOLS` ∪ the customer extension), * so internal callers honor customer protocols automatically. * * Pass `useStaticAllowlistOnly: true` to opt out of the customer extension when a call site * must reject any protocol the customer added. There is no current internal consumer of the * opt-out; it exists for future security-critical paths. */ public __internal_windowNavigate = (to: URL | string, opts?: { useStaticAllowlistOnly?: boolean }): void => { const allowedProtocols = opts?.useStaticAllowlistOnly ? ALLOWED_PROTOCOLS : this.#allowedRedirectProtocols; windowNavigate(to, { allowedProtocols }); }; #isLoaded(): this is LoadedClerk { return this.client !== undefined; } }