diff --git a/.changeset/flat-ravens-call.md b/.changeset/flat-ravens-call.md new file mode 100644 index 00000000000..4182328bd63 --- /dev/null +++ b/.changeset/flat-ravens-call.md @@ -0,0 +1,10 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/clerk-react': minor +'@clerk/vue': minor +--- + +Introduce in-app development prompt to enable the Organizations feature + +In development instances, when using organization components or hooks for the first time, developers will see a prompt to enable the Organizations feature directly in their app, eliminating the need to visit the Clerk Dashboard. diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 509aa6dfaba..acb9612d4a7 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,11 +1,11 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "840KB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "81KB" }, - { "path": "./dist/clerk.channel.browser.js", "maxSize": "81KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "123KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "83KB" }, + { "path": "./dist/clerk.channel.browser.js", "maxSize": "83KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "127KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "65KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "117.1KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "119KB" }, { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "122KB" }, { "path": "./dist/vendors*.js", "maxSize": "47KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, @@ -23,6 +23,7 @@ { "path": "./dist/onetap*.js", "maxSize": "1KB" }, { "path": "./dist/waitlist*.js", "maxSize": "1.5KB" }, { "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" }, + { "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" }, { "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" }, { "path": "./dist/checkout*.js", "maxSize": "8.82KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 1cccffbbcd8..d93d9bbfc20 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2508,4 +2508,128 @@ describe('Clerk singleton', () => { }); }); }); + + describe('__internal_attemptToEnableEnvironmentSetting', () => { + describe('for organizations', () => { + it('does not open prompt if organizations is enabled in development', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + organizationSettings: { + enabled: true, + }, + }), + ); + + const sut = new Clerk(productionPublishableKey); + + const __internal_openEnableOrganizationsPromptSpy = vi.fn(); + sut.__internal_openEnableOrganizationsPrompt = __internal_openEnableOrganizationsPromptSpy; + + await sut.load(); + + const result = await sut.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationSwitcher', + }); + + expect(result?.isEnabled).toBe(true); + expect(__internal_openEnableOrganizationsPromptSpy).not.toHaveBeenCalled(); + }); + + it('does not open prompt if organizations is enabled in production', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => true, + isDevelopmentOrStaging: () => true, + organizationSettings: { + enabled: true, + }, + }), + ); + + const sut = new Clerk(productionPublishableKey); + + const __internal_openEnableOrganizationsPromptSpy = vi.fn(); + sut.__internal_openEnableOrganizationsPrompt = __internal_openEnableOrganizationsPromptSpy; + + await sut.load(); + + const result = await sut.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationSwitcher', + }); + + expect(result?.isEnabled).toBe(true); + expect(__internal_openEnableOrganizationsPromptSpy).not.toHaveBeenCalled(); + }); + + it('opens prompt if organizations is disabled in development', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + organizationSettings: { + enabled: false, + }, + }), + ); + + const sut = new Clerk(developmentPublishableKey); + + const __internal_openEnableOrganizationsPromptSpy = vi.fn(); + sut.__internal_openEnableOrganizationsPrompt = __internal_openEnableOrganizationsPromptSpy; + + await sut.load(); + + const result = await sut.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationSwitcher', + }); + + expect(result?.isEnabled).toBe(false); + expect(__internal_openEnableOrganizationsPromptSpy).toHaveBeenCalled(); + }); + + it('does not open prompt if organizations is disabled in production', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + organizationSettings: { + enabled: false, + }, + }), + ); + + const sut = new Clerk(productionPublishableKey); + + const __internal_openEnableOrganizationsPromptSpy = vi.fn(); + sut.__internal_openEnableOrganizationsPrompt = __internal_openEnableOrganizationsPromptSpy; + + await sut.load(); + + const result = await sut.__internal_attemptToEnableEnvironmentSetting({ + for: 'organizations', + caller: 'OrganizationSwitcher', + }); + + expect(result?.isEnabled).toBe(false); + expect(__internal_openEnableOrganizationsPromptSpy).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 6353d4c09d3..e95a590c633 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -22,7 +22,10 @@ import { import type { __experimental_CheckoutInstance, __experimental_CheckoutOptions, + __internal_AttemptToEnableEnvironmentSettingParams, + __internal_AttemptToEnableEnvironmentSettingResult, __internal_CheckoutProps, + __internal_EnableOrganizationsPromptProps, __internal_OAuthConsentProps, __internal_PlanDetailsProps, __internal_SubscriptionDetailsProps, @@ -745,6 +748,56 @@ export class Clerk implements ClerkInterface { void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification')); }; + public __internal_attemptToEnableEnvironmentSetting = ( + params: __internal_AttemptToEnableEnvironmentSettingParams, + ): __internal_AttemptToEnableEnvironmentSettingResult => { + const { for: setting, caller } = params; + + if (!this.user) { + 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); + + 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.#componentControls); + void this.#componentControls + .ensureMounted({ preloadHint: 'EnableOrganizationsPrompt' }) + .then(controls => controls.openModal('enableOrganizationsPrompt', props || {})); + + this.telemetry?.record(eventPrebuiltComponentMounted('EnableOrganizationsPrompt', props)); + }; + + public __internal_closeEnableOrganizationsPrompt = (): void => { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted().then(controls => controls.closeModal('enableOrganizationsPrompt')); + }; + public __internal_openBlankCaptchaModal = (): Promise => { this.assertComponentsReady(this.#componentControls); return this.#componentControls @@ -816,14 +869,21 @@ export class Clerk implements ClerkInterface { public openOrganizationProfile = (props?: OrganizationProfileProps): void => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + 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.cannotRenderComponentWhenOrgDoesNotExist, { @@ -846,14 +906,21 @@ export class Clerk implements ClerkInterface { public openCreateOrganization = (props?: CreateOrganizationProps): void => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + 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; } + void this.#componentControls .ensureMounted({ preloadHint: 'CreateOrganization' }) .then(controls => controls.openModal('createOrganization', props || {})); @@ -988,14 +1055,21 @@ export class Clerk implements ClerkInterface { public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + 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') { @@ -1028,14 +1102,21 @@ export class Clerk implements ClerkInterface { public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + 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; } + void this.#componentControls?.ensureMounted({ preloadHint: 'CreateOrganization' }).then(controls => controls.mountComponent({ name: 'CreateOrganization', @@ -1059,14 +1140,21 @@ export class Clerk implements ClerkInterface { public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + 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; } + void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }).then(controls => controls.mountComponent({ name: 'OrganizationSwitcher', @@ -1098,14 +1186,21 @@ export class Clerk implements ClerkInterface { public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + + 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; } + void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationList' }).then(controls => controls.mountComponent({ name: 'OrganizationList', @@ -1294,12 +1389,17 @@ export class Clerk implements ClerkInterface { public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => { this.assertComponentsReady(this.#componentControls); - if (disabledOrganizationsFeature(this, this.environment)) { - if (this.#instanceType === 'development') { + 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; } diff --git a/packages/clerk-js/src/core/resources/DevTools.ts b/packages/clerk-js/src/core/resources/DevTools.ts new file mode 100644 index 00000000000..9a517858604 --- /dev/null +++ b/packages/clerk-js/src/core/resources/DevTools.ts @@ -0,0 +1,21 @@ +import type { ClerkResourceJSON, DevToolsResource, EnableEnvironmentSettingParams } from '@clerk/shared/types'; + +import { BaseResource } from './Base'; + +/** + * @internal + */ +export class DevTools extends BaseResource implements DevToolsResource { + pathRoot = '/dev_tools'; + + protected fromJSON(_data: ClerkResourceJSON | null): this { + return this; + } + + async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) { + await this._basePatch({ + path: `${this.pathRoot}/enable_environment_setting`, + body: params, + }); + } +} diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx index 148736665ce..6f3be02ea95 100644 --- a/packages/clerk-js/src/ui/Components.tsx +++ b/packages/clerk-js/src/ui/Components.tsx @@ -1,5 +1,6 @@ import type { __internal_CheckoutProps, + __internal_EnableOrganizationsPromptProps, __internal_PlanDetailsProps, __internal_SubscriptionDetailsProps, __internal_UserVerificationProps, @@ -27,6 +28,7 @@ import type { ClerkComponentName } from './lazyModules/components'; import { BlankCaptchaModal, CreateOrganizationModal, + EnableOrganizationsPrompt, ImpersonationFab, KeylessPrompt, OrganizationProfileModal, @@ -40,6 +42,7 @@ import { import { MountedCheckoutDrawer, MountedPlanDetailDrawer, MountedSubscriptionDetailDrawer } from './lazyModules/drawers'; import { LazyComponentRenderer, + LazyEnableOrganizationsPromptProvider, LazyImpersonationFabProvider, LazyModalRenderer, LazyOneTapRenderer, @@ -79,7 +82,8 @@ export type ComponentControls = { | 'createOrganization' | 'userVerification' | 'waitlist' - | 'blankCaptcha', + | 'blankCaptcha' + | 'enableOrganizationsPrompt', >( modal: T, props: T extends 'signIn' @@ -90,7 +94,9 @@ export type ComponentControls = { ? __internal_UserVerificationProps : T extends 'waitlist' ? WaitlistProps - : UserProfileProps, + : T extends 'enableOrganizationsPrompt' + ? __internal_EnableOrganizationsPromptProps + : UserProfileProps, ) => void; closeModal: ( modal: @@ -102,7 +108,8 @@ export type ComponentControls = { | 'createOrganization' | 'userVerification' | 'waitlist' - | 'blankCaptcha', + | 'blankCaptcha' + | 'enableOrganizationsPrompt', options?: { notify?: boolean; }, @@ -152,6 +159,7 @@ interface ComponentsState { userVerificationModal: null | __internal_UserVerificationProps; organizationProfileModal: null | OrganizationProfileProps; createOrganizationModal: null | CreateOrganizationProps; + enableOrganizationsPromptModal: null | __internal_EnableOrganizationsPromptProps; blankCaptchaModal: null; organizationSwitcherPrefetch: boolean; waitlistModal: null | WaitlistProps; @@ -245,6 +253,7 @@ const Components = (props: ComponentsProps) => { userVerificationModal: null, organizationProfileModal: null, createOrganizationModal: null, + enableOrganizationsPromptModal: null, organizationSwitcherPrefetch: false, waitlistModal: null, blankCaptchaModal: null, @@ -325,9 +334,10 @@ const Components = (props: ComponentsProps) => { clearUrlStateParam(); setState(s => { function handleCloseModalForExperimentalUserVerification() { - const modal = s[`${name}Modal`] || {}; + const modal = s[`${name}Modal`]; if (modal && typeof modal === 'object' && 'afterVerificationCancelled' in modal && notify) { - modal.afterVerificationCancelled?.(); + // TypeScript doesn't narrow properly with template literal access and 'in' operator + (modal as { afterVerificationCancelled?: () => void }).afterVerificationCancelled?.(); } } @@ -342,6 +352,20 @@ const Components = (props: ComponentsProps) => { }; componentsControls.openModal = (name, props) => { + // Prevent opening enableOrganizations prompt if it's already open + // It should open the first call and ignore the subsequent calls + if (name === 'enableOrganizationsPrompt') { + setState(prev => { + // Modal is already open, don't update state + if (prev.enableOrganizationsPromptModal) { + return prev; + } + + return { ...prev, [`${name}Modal`]: props }; + }); + return; + } + function handleCloseModalForExperimentalUserVerification() { if (!('afterVerificationCancelled' in props)) { return; @@ -612,6 +636,12 @@ const Components = (props: ComponentsProps) => { )} + {state.enableOrganizationsPromptModal && ( + + + + )} + {state.options?.__internal_keyless_claimKeylessApplicationUrl && state.options?.__internal_keyless_copyInstanceKeysUrl && ( diff --git a/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx b/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx index f60898e19d2..3d560ab09b6 100644 --- a/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx +++ b/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx @@ -1,5 +1,10 @@ import { isClerkAPIResponseError } from '@clerk/shared/error'; -import { __experimental_useAPIKeys as useAPIKeys, useClerk, useOrganization, useUser } from '@clerk/shared/react'; +import { + __experimental_useAPIKeys as useAPIKeys, + useClerk, + useOrganizationContext, + useUser, +} from '@clerk/shared/react'; import type { APIKeyResource } from '@clerk/shared/types'; import { lazy, useState } from 'react'; @@ -234,9 +239,10 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr const _APIKeys = () => { const ctx = useAPIKeysContext(); const { user } = useUser(); - const { organization } = useOrganization(); + // Do not use `useOrganization` to avoid triggering the in-app enable organizations prompt in development instance + const organizationCtx = useOrganizationContext(); - const subject = organization?.id ?? user?.id ?? ''; + const subject = organizationCtx?.organization?.id ?? user?.id ?? ''; return ( - - - - - - - - - - - - - - - - - - - ); -} diff --git a/packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx b/packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx new file mode 100644 index 00000000000..b9f69180db2 --- /dev/null +++ b/packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx @@ -0,0 +1,667 @@ +import { useClerk } from '@clerk/shared/react'; +import type { __internal_EnableOrganizationsPromptProps, EnableEnvironmentSettingParams } from '@clerk/shared/types'; +// eslint-disable-next-line no-restricted-imports +import type { SerializedStyles } from '@emotion/react'; +// eslint-disable-next-line no-restricted-imports +import { css, type Theme } from '@emotion/react'; +import { forwardRef, useId, useLayoutEffect, useRef, useState } from 'react'; + +import { useEnvironment } from '@/ui/contexts'; +import { Modal } from '@/ui/elements/Modal'; +import { common, InternalThemeProvider } from '@/ui/styledSystem'; + +import { DevTools } from '../../../../core/resources/DevTools'; +import { Box, Flex, Span } from '../../../customizables'; +import { Portal } from '../../../elements/Portal'; +import { basePromptElementStyles, ClerkLogoIcon, PromptContainer, PromptSuccessIcon } from '../shared'; + +const organizationsDashboardUrl = 'https://dashboard.clerk.com/~/organizations-settings'; + +const EnableOrganizationsPromptInternal = ({ + caller, + onSuccess, + onClose, +}: __internal_EnableOrganizationsPromptProps) => { + const clerk = useClerk(); + const [isLoading, setIsLoading] = useState(false); + const [isEnabled, setIsEnabled] = useState(false); + const [allowPersonalAccount, setAllowPersonalAccount] = useState(false); + + const initialFocusRef = useRef(null); + const environment = useEnvironment(); + + const isComponent = !caller.startsWith('use'); + + // 'forceOrganizationSelection' is omitted from the environment settings object if the instance does not have it available as a feature + const hasPersonalAccountsEnabled = + typeof environment?.organizationSettings.forceOrganizationSelection !== 'undefined'; + + const handleEnableOrganizations = () => { + setIsLoading(true); + + const params: EnableEnvironmentSettingParams = { + enable_organizations: true, + }; + + if (hasPersonalAccountsEnabled) { + params.organization_allow_personal_accounts = allowPersonalAccount; + } + + void new DevTools() + .__internal_enableEnvironmentSetting(params) + .then(() => { + setIsEnabled(true); + setIsLoading(false); + }) + .catch(() => { + setIsLoading(false); + }); + }; + + return ( + + ({ alignItems: 'center' })} + initialFocusRef={initialFocusRef} + > + ({ + display: 'flex', + flexDirection: 'column', + width: '30rem', + maxWidth: 'calc(100vw - 2rem)', + })} + > + ({ + padding: `${t.sizes.$4} ${t.sizes.$6}`, + paddingBottom: t.sizes.$4, + gap: t.sizes.$2, + })} + > + ({ + gap: t.sizes.$2, + })} + > + + +

+ {isEnabled ? 'Organizations feature enabled' : 'Organizations feature required'} +

+
+ + ({ + gap: t.sizes.$0x5, + })} + > + {isEnabled ? ( +

+ {clerk.user + ? `The Organizations feature has been enabled for your application. A default organization named "My Organization" was created automatically. You can manage or rename it in your` + : `The Organizations feature has been enabled for your application. You can manage it in your`}{' '} + + dashboard + + . +

+ ) : ( + <> +

+ Enable Organizations to use{' '} + + {isComponent ? `<${caller} />` : caller} + {' '} +

+ + + Learn more + + + )} +
+ + {hasPersonalAccountsEnabled && ( + ({ + display: 'grid', + gridTemplateRows: isEnabled ? '0fr' : '1fr', + transition: `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`, + marginInline: '-0.5rem', + overflow: 'hidden', + })} + {...(isEnabled && { inert: '' })} + > + ({ + minHeight: 0, + paddingInline: '0.5rem', + opacity: isEnabled ? 0 : 1, + transition: `opacity ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`, + })} + > + ({ marginTop: t.sizes.$2 })}> + setAllowPersonalAccount(prev => !prev)} + /> + + + + )} +
+ + + + ({ + padding: `${t.sizes.$4} ${t.sizes.$6}`, + gap: t.sizes.$3, + justifyContent: 'flex-end', + })} + > + {isEnabled ? ( + { + if (!clerk.user) { + void clerk.redirectToSignIn(); + clerk.__internal_closeEnableOrganizationsPrompt?.(); + } else { + onSuccess?.(); + } + }} + > + {clerk.user ? 'Continue' : 'Sign in to continue'} + + ) : ( + <> + { + clerk?.__internal_closeEnableOrganizationsPrompt?.(); + onClose?.(); + }} + > + I'll remove it myself + + + + Enable Organizations + + + )} + +
+
+
+ ); +}; + +/** + * A prompt that allows the user to enable the Organizations feature for their development instance + * @internal + */ +export const EnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps) => { + return ( + + + + ); +}; + +const baseButtonStyles = css` + ${basePromptElementStyles}; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + height: 1.75rem; + padding: 0.375rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.12px; + color: white; + text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.32); + white-space: nowrap; + user-select: none; + color: white; + outline: none; + + &:not(:disabled) { + transition: 120ms ease-in-out; + transition-property: background-color, border-color, box-shadow, color; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &:focus-visible:not(:disabled) { + outline: 2px solid white; + outline-offset: 2px; + } +`; + +const buttonSolidStyles = css` + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.5%, rgba(0, 0, 0, 0.05) 100%), #454545; + box-shadow: + 0 0 3px 0 rgba(253, 224, 71, 0) inset, + 0 0 0 1px rgba(255, 255, 255, 0.04) inset, + 0 1px 0 0 rgba(255, 255, 255, 0.04) inset, + 0 0 0 1px rgba(0, 0, 0, 0.12), + 0 1.5px 2px 0 rgba(0, 0, 0, 0.48); + + &:hover:not(:disabled) { + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.5%, rgba(0, 0, 0, 0.15) 100%), #5f5f5f; + box-shadow: + 0 0 3px 0 rgba(253, 224, 71, 0) inset, + 0 0 0 1px rgba(255, 255, 255, 0.04) inset, + 0 1px 0 0 rgba(255, 255, 255, 0.04) inset, + 0 0 0 1px rgba(0, 0, 0, 0.12), + 0 1.5px 2px 0 rgba(0, 0, 0, 0.48); + } +`; + +const buttonOutlineStyles = css` + border: 1px solid rgba(118, 118, 132, 0.25); + background: rgba(69, 69, 69, 0.1); + + &:hover:not(:disabled) { + border-color: rgba(118, 118, 132, 0.5); + } +`; + +const buttonVariantStyles = { + solid: buttonSolidStyles, + outline: buttonOutlineStyles, +} as const; + +type PromptButtonVariant = keyof typeof buttonVariantStyles; + +type PromptButtonProps = Pick, 'onClick' | 'children' | 'disabled'> & { + variant?: PromptButtonVariant; +}; + +const PromptButton = forwardRef(({ variant = 'solid', ...props }, ref) => { + return ( +