From 5112bde160a91868c918186a69dabbf0b996497c Mon Sep 17 00:00:00 2001 From: George Desipris Date: Tue, 12 Mar 2024 11:05:41 +0200 Subject: [PATCH] feat(clerk-js): Throw errors for invalid mounts/modal opens in development --- .changeset/great-turtles-reply.md | 5 ++ packages/clerk-js/src/core/clerk.ts | 87 +++++++++++++++---- packages/clerk-js/src/core/warnings.ts | 2 +- packages/clerk-js/src/ui/common/index.ts | 1 - .../common/withOrganizationsEnabledGuard.tsx | 29 ------- .../CreateOrganization/CreateOrganization.tsx | 7 +- .../OrganizationList/OrganizationList.tsx | 5 +- .../OrganizationProfile.tsx | 9 +- .../OrganizationSwitcher.tsx | 7 +- 9 files changed, 79 insertions(+), 73 deletions(-) create mode 100644 .changeset/great-turtles-reply.md delete mode 100644 packages/clerk-js/src/ui/common/withOrganizationsEnabledGuard.tsx diff --git a/.changeset/great-turtles-reply.md b/.changeset/great-turtles-reply.md new file mode 100644 index 0000000000..02f61ea1af --- /dev/null +++ b/.changeset/great-turtles-reply.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': minor +--- + +Throw an error in development when there is an invalid mount or modal open. This includes mounting a component when the resource is not available (i.e. `mountUserProfile()` when the user does not exist) as well as mounting a component without the feature being enabled via the clerk dashboard (i.e. `mountOrganizationProfile()` without having organizations enabled). diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 42234c9efc..c78cca7f4a 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,5 +1,6 @@ import { addClerkPrefix, + ClerkRuntimeError, handleValueOrFn, inBrowser as inClientSide, is4xxError, @@ -331,8 +332,12 @@ export class Clerk implements ClerkInterface { public openSignIn = (props?: SignInProps): void => { this.assertComponentsReady(this.#componentControls); - if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment) && this.#instanceType === 'development') { - console.info(warnings.cannotOpenSignUpOrSignUp); + if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { + code: 'cannot_open_single_session_enabled', + }); + } return; } void this.#componentControls @@ -347,8 +352,13 @@ export class Clerk implements ClerkInterface { public openSignUp = (props?: SignUpProps): void => { this.assertComponentsReady(this.#componentControls); - if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment) && this.#instanceType === 'development') { - return console.info(warnings.cannotOpenSignUpOrSignUp); + if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { + code: 'cannot_open_single_session_enabled', + }); + } + return; } void this.#componentControls .ensureMounted({ preloadHint: 'SignUp' }) @@ -362,8 +372,13 @@ export class Clerk implements ClerkInterface { public openUserProfile = (props?: UserProfileProps): void => { this.assertComponentsReady(this.#componentControls); - if (noUserExists(this) && this.#instanceType === 'development') { - return console.info(warnings.cannotOpenUserProfile); + if (noUserExists(this)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, { + code: 'cannot_open_no_user', + }); + } + return; } void this.#componentControls .ensureMounted({ preloadHint: 'UserProfile' }) @@ -378,11 +393,19 @@ export class Clerk implements ClerkInterface { public openOrganizationProfile = (props?: OrganizationProfileProps): void => { this.assertComponentsReady(this.#componentControls); if (disabledOrganizationsFeature(this, this.#environment)) { - console.info(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile')); + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { + code: 'cannot_open_organizations_disabled', + }); + } return; } - if (noOrganizationExists(this) && this.#instanceType === 'development') { - console.info(warnings.cannotRenderComponentWhenOrgDoesNotExist); + if (noOrganizationExists(this)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenOrgDoesNotExist, { + code: 'cannot_open_no_organization', + }); + } return; } void this.#componentControls @@ -398,7 +421,11 @@ export class Clerk implements ClerkInterface { public openCreateOrganization = (props?: CreateOrganizationProps): void => { this.assertComponentsReady(this.#componentControls); if (disabledOrganizationsFeature(this, this.#environment)) { - console.info(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization')); + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { + code: 'cannot_open_organizations_disabled', + }); + } return; } void this.#componentControls @@ -457,8 +484,12 @@ export class Clerk implements ClerkInterface { public mountUserProfile = (node: HTMLDivElement, props?: UserProfileProps): void => { this.assertComponentsReady(this.#componentControls); - if (noUserExists(this) && this.#instanceType === 'development') { - console.info(warnings.cannotRenderComponentWhenUserDoesNotExist); + if (noUserExists(this)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenUserDoesNotExist, { + code: 'cannot_open_no_user', + }); + } return; } void this.#componentControls.ensureMounted({ preloadHint: 'UserProfile' }).then(controls => @@ -485,11 +516,19 @@ export class Clerk implements ClerkInterface { public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { this.assertComponentsReady(this.#componentControls); if (disabledOrganizationsFeature(this, this.#environment)) { - console.info(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile')); + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), { + code: 'cannot_open_organizations_disabled', + }); + } return; } - if (noOrganizationExists(this) && this.#instanceType === 'development') { - console.info(warnings.cannotRenderComponentWhenOrgDoesNotExist); + if (noOrganizationExists(this)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenOrgDoesNotExist, { + code: 'cannot_open_no_organization', + }); + } return; } void this.#componentControls.ensureMounted({ preloadHint: 'OrganizationProfile' }).then(controls => @@ -516,7 +555,11 @@ export class Clerk implements ClerkInterface { public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { this.assertComponentsReady(this.#componentControls); if (disabledOrganizationsFeature(this, this.#environment)) { - console.info(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization')); + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), { + code: 'cannot_open_organizations_disabled', + }); + } return; } void this.#componentControls?.ensureMounted({ preloadHint: 'CreateOrganization' }).then(controls => @@ -543,7 +586,11 @@ export class Clerk implements ClerkInterface { public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { this.assertComponentsReady(this.#componentControls); if (disabledOrganizationsFeature(this, this.#environment)) { - console.info(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher')); + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher'), { + code: 'cannot_open_organizations_disabled', + }); + } return; } void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }).then(controls => @@ -566,7 +613,11 @@ export class Clerk implements ClerkInterface { public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { this.assertComponentsReady(this.#componentControls); if (disabledOrganizationsFeature(this, this.#environment)) { - console.info(warnings.cannotRenderAnyOrganizationComponent('OrganizationList')); + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationList'), { + code: 'cannot_open_organizations_disabled', + }); + } return; } void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationList' }).then(controls => diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts index fe5c155e70..1fd500a1e9 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -24,7 +24,7 @@ const warnings = { cannotRenderAnyOrganizationComponent: createMessageForDisabledOrganizations, cannotOpenUserProfile: 'The UserProfile modal cannot render unless a user is signed in. Since no user is signed in, this is no-op.', - cannotOpenSignUpOrSignUp: + cannotOpenSignInOrSignUp: 'The SignIn or SignUp modals do not render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, this is no-op.', }; diff --git a/packages/clerk-js/src/ui/common/index.ts b/packages/clerk-js/src/ui/common/index.ts index ba3460a536..95597333ac 100644 --- a/packages/clerk-js/src/ui/common/index.ts +++ b/packages/clerk-js/src/ui/common/index.ts @@ -15,5 +15,4 @@ export * from './RemoveResourceForm'; export * from './PrintableComponent'; export * from './NotificationCountBadge'; export * from './RemoveResourceForm'; -export * from './withOrganizationsEnabledGuard'; export * from './QRCode'; diff --git a/packages/clerk-js/src/ui/common/withOrganizationsEnabledGuard.tsx b/packages/clerk-js/src/ui/common/withOrganizationsEnabledGuard.tsx deleted file mode 100644 index 72b8c66046..0000000000 --- a/packages/clerk-js/src/ui/common/withOrganizationsEnabledGuard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -import { useEnvironment } from '../contexts'; -import { useRouter } from '../router'; - -export function withOrganizationsEnabledGuard

( - WrappedComponent: React.ComponentType

, - name: string, - options: { mode: 'redirect' | 'hide' }, -): React.ComponentType

{ - const Hoc = (props: P) => { - const { navigate } = useRouter(); - const { organizationSettings, displayConfig } = useEnvironment(); - - React.useEffect(() => { - if (options.mode === 'redirect' && !organizationSettings.enabled) { - void navigate(displayConfig.homeUrl); - } - }, []); - - if (options.mode === 'hide' && !organizationSettings.enabled) { - return null; - } - - return ; - }; - Hoc.displayName = name; - return Hoc; -} diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx index e0ed90ea9b..559555c0bd 100644 --- a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx +++ b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganization.tsx @@ -1,6 +1,5 @@ import type { CreateOrganizationModalProps } from '@clerk/types'; -import { withOrganizationsEnabledGuard } from '../../common'; import { ComponentContext, withCoreUserGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { withCardStateProvider } from '../../elements'; @@ -26,11 +25,7 @@ const AuthenticatedRoutes = withCoreUserGuard(() => { return ; }); -export const CreateOrganization = withOrganizationsEnabledGuard( - withCardStateProvider(_CreateOrganization), - 'CreateOrganization', - { mode: 'redirect' }, -); +export const CreateOrganization = withCardStateProvider(_CreateOrganization); export const CreateOrganizationModal = (props: CreateOrganizationModalProps): JSX.Element => { const createOrganizationProps: CreateOrganizationCtx = { diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationList.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationList.tsx index b36a7bcdd4..69bb2fb025 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationList.tsx @@ -1,4 +1,3 @@ -import { withOrganizationsEnabledGuard } from '../../common'; import { withCoreUserGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch } from '../../router'; @@ -20,6 +19,4 @@ const _OrganizationList = () => { const AuthenticatedRoutes = withCoreUserGuard(OrganizationListPage); -export const OrganizationList = withOrganizationsEnabledGuard(_OrganizationList, 'OrganizationList', { - mode: 'redirect', -}); +export const OrganizationList = _OrganizationList; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx index 911c3fbca5..e540dacf64 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfile.tsx @@ -2,7 +2,6 @@ import { useOrganization } from '@clerk/shared/react'; import type { OrganizationProfileModalProps, OrganizationProfileProps } from '@clerk/types'; import React from 'react'; -import { withOrganizationsEnabledGuard } from '../../common'; import { ComponentContext, withCoreUserGuard } from '../../contexts'; import { Flow, localizationKeys } from '../../customizables'; import { NavbarMenuButtonRow, ProfileCard, withCardStateProvider } from '../../elements'; @@ -47,13 +46,7 @@ const AuthenticatedRoutes = withCoreUserGuard(() => { ); }); -export const OrganizationProfile = withOrganizationsEnabledGuard( - withCardStateProvider(_OrganizationProfile), - 'OrganizationProfile', - { - mode: 'redirect', - }, -); +export const OrganizationProfile = withCardStateProvider(_OrganizationProfile); export const OrganizationProfileModal = (props: OrganizationProfileModalProps): JSX.Element => { const organizationProfileProps: OrganizationProfileCtx = { diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx index bf29de404a..b066480dec 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -1,6 +1,5 @@ import { useId } from 'react'; -import { withOrganizationsEnabledGuard } from '../../common'; import { AcceptedInvitationsProvider, withCoreUserGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Popover, withCardStateProvider, withFloatingTree } from '../../elements'; @@ -46,8 +45,4 @@ const _OrganizationSwitcher = withFloatingTree(() => { ); }); -export const OrganizationSwitcher = withOrganizationsEnabledGuard( - withCoreUserGuard(withCardStateProvider(_OrganizationSwitcher)), - 'OrganizationSwitcher', - { mode: 'hide' }, -); +export const OrganizationSwitcher = withCoreUserGuard(withCardStateProvider(_OrganizationSwitcher));